第 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
按这个顺序排查:
RegisterRouters()是否调用了route.InitApi;InitApi是否真的执行(Invoke是否报错被忽略);router.RouterServiceProvider是否已注册;- 路由是否注册在
/api/v1分组下; - 请求方法是否与注册时一致。
9. 如果启动报缺依赖
按这个顺序排查:
Binds()是否包含 Handle、Service、Repository;ServiceProviders()是否包含redis.RedisServiceProvider;validator由框架基础 Provider 提供,不需要重复注册;- 构造函数参数类型是否写成了接口,而实现没有绑定到该接口。
自检
- [ ] 每一层只做自己的事:Handle 绑定/返回,Service 业务,Repository 数据;
- [ ] 写操作有锁,业务错误用
BizError; - [ ] 路由通过 Builder 注册,访问级别正确;
- [ ] 菜单
ComponentName与前端组件name一致; - [ ] 模型已加入
RegisterMigrate(); - [ ] 创建、列表两条主链路都验证通过。
评论
加载评论中…