文档目录
glen
· 预计阅读 7 分钟

第 8 章 从零创建子系统实战:订单模块

本文导航
第 8 章 从零创建子系统实战:订单模块

第 8 章 从零创建子系统实战:订单模块

前面几章拆开讲了契约、DI、路由、配置和迁移。这一章把它们拼起来,做一个能创建、能查询的订单子系统。目标不是功能多,而是每一层都接线正确。

目录结构

my-order/
├── main.go
├── go.mod
└── app/
    ├── app.go                    # SubApp 装配
    ├── route/
    │   └── api.go                # 路由、菜单
    ├── model/
    │   └── order.go              # 模型
    ├── repository/
    │   └── order.go              # 数据访问
    ├── service/
    │   └── order_service.go      # 业务逻辑
    ├── handle/
    │   └── v1/
    │       └── order_handle.go   # HTTP 入口
    └── database/
        └── auto_migrate.go       # 待迁移模型

1. 模型

// app/model/order.go
package model

import (
	"bit-labs.cn/owl/provider/db"
	"gorm.io/gorm/schema"
)

const (
	OrderStatusPending = 1
	OrderStatusPaid    = 2
)

var _ schema.Tabler = (*Order)(nil)

type Order struct {
	db.BaseModel
	OrderNo string `gorm:"comment:订单号;size:64;uniqueIndex" json:"orderNo"`
	Title   string `gorm:"comment:标题;size:255" json:"title"`
	Amount  int64  `gorm:"comment:金额,分" json:"amount"`
	Status  int    `gorm:"comment:状态" json:"status"`
}

func (i *Order) TableName() string { return "order_order" }

2. Repository

// app/repository/order.go
package repository

import (
	"context"

	"bit-labs.cn/my-order/app/model"
	"bit-labs.cn/owl/provider/db"
	"gorm.io/gorm"
)

type OrderRepositoryInterface interface {
	Save(o *model.Order) error
	Unique(id uint, fn func(db *gorm.DB)) (*model.Order, bool)
	Retrieve(page, pageSize int, fn func(db *gorm.DB)) (int64, []model.Order, error)
	WithContext(ctx context.Context) OrderRepositoryInterface
}

type OrderRepository struct {
	db.BaseRepository[model.Order]
	ctx context.Context
}

func NewOrderRepository(gdb *gorm.DB) *OrderRepository {
	return &OrderRepository{BaseRepository: db.NewBaseRepository[model.Order](gdb)}
}

func (i *OrderRepository) WithContext(ctx context.Context) OrderRepositoryInterface {
	i.BaseRepository.WithContext(ctx)
	i.ctx = ctx
	return i
}

func (i *OrderRepository) Save(o *model.Order) error {
	return i.BaseRepository.Save(o)
}

func (i *OrderRepository) Unique(id uint, fn func(db *gorm.DB)) (*model.Order, bool) {
	return i.BaseRepository.Unique(id, fn)
}

func (i *OrderRepository) Retrieve(page, pageSize int, fn func(db *gorm.DB)) (int64, []model.Order, error) {
	return i.BaseRepository.Retrieve(page, pageSize, fn)
}

3. Service

// app/service/order_service.go
package service

import (
	"context"

	"bit-labs.cn/my-order/app/model"
	"bit-labs.cn/my-order/app/repository"
	errContract "bit-labs.cn/owl/contract/errors"
	"bit-labs.cn/owl/provider/db"
	"bit-labs.cn/owl/provider/redis"
	"bit-labs.cn/owl/provider/router"
	validatorv10 "github.com/go-playground/validator/v10"
	"gorm.io/gorm"
)

const CodeOrderExists = "ORDER_EXISTS"

func OrderExists() *errContract.BizError {
	return errContract.NewBizError(CodeOrderExists, "订单号已存在")
}

type CreateOrderReq struct {
	OrderNo string `json:"orderNo" validate:"required,max=64" label:"订单号"`
	Title   string `json:"title" validate:"required,max=255" label:"标题"`
	Amount  int64  `json:"amount" validate:"gte=0" label:"金额"`
}

type RetrieveOrderReq struct {
	router.PageReq
	OrderNo string `form:"orderNo" json:"orderNo"`
	Status  int    `form:"status" json:"status"`
}

type OrderService struct {
	repo     repository.OrderRepositoryInterface
	locker   redis.LockerFactory
	validate *validatorv10.Validate
}

func NewOrderService(
	repo repository.OrderRepositoryInterface,
	locker redis.LockerFactory,
	validate *validatorv10.Validate,
) *OrderService {
	return &OrderService{repo: repo, locker: locker, validate: validate}
}

func (i *OrderService) CreateOrder(ctx context.Context, req *CreateOrderReq) error {
	if err := i.validate.Struct(req); err != nil {
		return err
	}

	l := i.locker.New()
	if err := l.Lock("order:create:" + req.OrderNo); err != nil {
		return err
	}
	defer l.Unlock()

	exists, _ := i.repo.WithContext(ctx).Unique(0, func(tx *gorm.DB) {
		tx.Where("order_no = ?", req.OrderNo)
	})
	if exists {
		return OrderExists()
	}

	order := &model.Order{
		OrderNo: req.OrderNo,
		Title:   req.Title,
		Amount:  req.Amount,
		Status:  model.OrderStatusPending,
	}
	return i.repo.WithContext(ctx).Save(order)
}

func (i *OrderService) RetrieveOrders(ctx context.Context, req *RetrieveOrderReq) (int64, []model.Order, error) {
	if err := i.validate.Struct(req); err != nil {
		return 0, nil, err
	}
	return i.repo.WithContext(ctx).Retrieve(req.Page, req.PageSize, func(tx *gorm.DB) {
		db.AppendWhereFromStruct(tx, req)
		tx.Order("created_at desc")
	})
}

这里用了三种框架能力:

  • validator:请求结构体上的 validate 标签;
  • redis.LockerFactory:写操作加分布式锁;
  • errContract.BizError:业务错误,router.Fail 会原样返回给前端。

Unique 由 BaseRepository 提供,仓储层只做转发。

4. Handle

// app/handle/v1/order_handle.go
package v1

import (
	"bit-labs.cn/my-order/app/service"
	"bit-labs.cn/owl/provider/router"
	"github.com/gin-gonic/gin"
)

type OrderHandle struct {
	svc *service.OrderService
}

func NewOrderHandle(svc *service.OrderService) *OrderHandle {
	return &OrderHandle{svc: svc}
}

func (i *OrderHandle) ModuleName() (string, string) {
	return "order", "订单管理"
}

func (i *OrderHandle) Create(ctx *gin.Context) {
	var req service.CreateOrderReq
	if err := ctx.ShouldBindJSON(&req); err != nil {
		router.Fail(ctx, err)
		return
	}
	if err := i.svc.CreateOrder(ctx.Request.Context(), &req); err != nil {
		router.Fail(ctx, err)
		return
	}
	router.Success(ctx, nil)
}

func (i *OrderHandle) Retrieve(ctx *gin.Context) {
	var req service.RetrieveOrderReq
	if err := ctx.ShouldBindQuery(&req); err != nil {
		router.BadRequest(ctx, "参数绑定失败")
		return
	}
	count, list, err := i.svc.RetrieveOrders(ctx.Request.Context(), &req)
	if err != nil {
		router.Fail(ctx, err)
		return
	}
	router.PageSuccess(ctx, int(count), req.Page, req.PageSize, list)
}

Handle 只做三件事:绑定参数、调用 Service、返回统一响应。业务规则不要写进 Handle。

5. 路由与菜单

// app/route/api.go
package route

import (
	"bit-labs.cn/my-order/app/handle/v1"
	"bit-labs.cn/owl"
	"bit-labs.cn/owl/contract/foundation"
	"bit-labs.cn/owl/provider/router"
	"github.com/gin-gonic/gin"
)

var orderMenu *router.Menu

func InitMenu() []*router.Menu {
	return []*router.Menu{
		{
			Path: "/order",
			Name: "Order",
			Meta: router.Meta{
				Title:      "订单管理",
				Icon:       "ep:shopping-cart",
				ShowParent: true,
				ShowLink:   true,
			},
			MenuType: router.MenuTypeDir,
			Children: []*router.Menu{orderMenu},
		},
	}
}

func InitApi(app foundation.Application, appName string) {
	err := app.Invoke(func(engine *gin.Engine, handle *v1.OrderHandle) {
		gv1 := engine.Group("/api/v1")

		r := router.NewRouteInfoBuilder(appName, handle, gv1, router.MenuOption{
			ComponentName: "OrderList",
			Path:          "/order/list",
			Icon:          "ep:list",
		})

		r.Post("/orders", router.AccessAuthorized, handle.Create).Name("创建订单").Build()
		r.Get("/orders", router.AccessAuthorized, handle.Retrieve).Name("订单列表").Build()

		orderMenu = r.GetMenu()
	})
	owl.PanicIf(err)
}

6. 装配

// app/app.go
package app

import (
	"my-order/app/handle/v1"
	"my-order/app/model"
	"my-order/app/repository"
	"my-order/app/route"
	"my-order/app/service"

	"bit-labs.cn/owl"
	"bit-labs.cn/owl/contract/foundation"
	"bit-labs.cn/owl/provider/db"
	"bit-labs.cn/owl/provider/redis"
	"bit-labs.cn/owl/provider/router"
	"github.com/spf13/cobra"
	"gorm.io/gorm"
)

type SubAppOrder struct {
	app foundation.Application
}

func (i *SubAppOrder) Name() string { return "order" }

func (i *SubAppOrder) ServiceProviders() []foundation.ServiceProvider {
	return []foundation.ServiceProvider{
		&router.RouterServiceProvider{},
		&db.DBServiceProvider{},
		&redis.RedisServiceProvider{},
	}
}

func (i *SubAppOrder) Binds() []any {
	return []any{
		v1.NewOrderHandle,
		service.NewOrderService,
		repository.NewOrderRepository,
	}
}

func (i *SubAppOrder) RegisterRouters() {
	route.InitApi(i.app, i.Name())
}

func (i *SubAppOrder) RegisterMenus() []*router.Menu {
	return route.InitMenu()
}

func (i *SubAppOrder) RegisterMigrate() []any {
	return []any{&model.Order{}}
}

func (i *SubAppOrder) Bootstrap() {}

func (i *SubAppOrder) RegisterCommands() []*cobra.Command { return nil }

func (i *SubAppOrder) BeforeMigrate(db *gorm.DB) error { return nil }

func (i *SubAppOrder) AfterMigrate(db *gorm.DB) error { return nil }

上面的 app.go 已经 import 了 my-order/app/model。

7. 启动并验证

go run .
curl -X POST "http://127.0.0.1:8080/api/v1/orders" ^
  -H "Content-Type: application/json" ^
  -d "{\"orderNo\":\"SO001\",\"title\":\"首单\",\"amount\":100}"

curl "http://127.0.0.1:8080/api/v1/orders?page=1&pageSize=10"

期望:

  • 创建接口返回 success: true;
  • 列表接口返回 data.list,包含刚创建的订单;
  • 数据库中出现 order_order 表;
  • storage/migrate_hash.txt 中出现 Order 的 hash。

8. 如果接口 404

按这个顺序排查:

  1. RegisterRouters() 是否调用了 route.InitApi;
  2. InitApi 是否真的执行(Invoke 是否报错被忽略);
  3. router.RouterServiceProvider 是否已注册;
  4. 路由是否注册在 /api/v1 分组下;
  5. 请求方法是否与注册时一致。

9. 如果启动报缺依赖

按这个顺序排查:

  1. Binds() 是否包含 Handle、Service、Repository;
  2. ServiceProviders() 是否包含 redis.RedisServiceProvider;
  3. validator 由框架基础 Provider 提供,不需要重复注册;
  4. 构造函数参数类型是否写成了接口,而实现没有绑定到该接口。

自检

  • [ ] 每一层只做自己的事:Handle 绑定/返回,Service 业务,Repository 数据;
  • [ ] 写操作有锁,业务错误用 BizError;
  • [ ] 路由通过 Builder 注册,访问级别正确;
  • [ ] 菜单 ComponentName 与前端组件 name 一致;
  • [ ] 模型已加入 RegisterMigrate();
  • [ ] 创建、列表两条主链路都验证通过。

评论

加载评论中…

上一篇 第 7 章 数据模型与迁移 下一篇 第 9 章 常见坑与上线自检

BitLabs © 2026 · 论坛讨论 · 博客长文 · 文档手册