Skip to content

模块架构

Go 模块化单体 · 同进程 interface 调用 · 清晰数据 ownership · worker 只通过数据库事件协作


核心理念

后端不拆成多个 K8s 微服务。api-serveradmin-apiworker 是三个 Go 进程,但业务边界在同一仓库的 internal/* 模块里。

text
┌─────────────────────────────────────────────────────┐
│                  Go 后端代码仓库                      │
│                                                     │
│  cmd/api-server     cmd/admin-api     cmd/worker    │
│        │                 │                 │        │
│        └───────────┬─────┴─────┬───────────┘        │
│                    ▼           ▼                    │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐             │
│  │  auth    │ │  user    │ │ catalog  │             │
│  └──────────┘ └──────────┘ └──────────┘             │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐             │
│  │inventory │ │  order   │ │ payment  │             │
│  └──────────┘ └──────────┘ └──────────┘             │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐             │
│  │ coupon   │ │delivery  │ │  asset   │             │
│  └──────────┘ └──────────┘ └──────────┘             │
│  ┌──────────────────────────────────────┐           │
│  │ shared: db/cache/config/logger       │           │
│  │ middleware/outbox/audit/idempotency  │           │
│  └──────────────────────────────────────┘           │
└─────────────────────────────────────────────────────┘

模块之间不是网络边界,而是代码边界。这样可以保留拆分时需要的接口纪律,同时避免微服务带来的部署、排障、事务和上下文成本。


模块清单

模块目录职责主要入口
authinternal/auth/微信登录、密码登录、JWT、token version、后台账号权限HTTP handler + service
userinternal/user/用户资料、收货地址、会员积分HTTP handler + service
cataloginternal/catalog/商品、分类、溯源、商品图引用、基础搜索HTTP handler + admin handler
inventoryinternal/inventory/批次库存、锁定、释放、扣减、库存流水service + admin handler
orderinternal/order/订单状态机、计价、订单项、售后入口、Saga 编排HTTP handler + worker handler
paymentinternal/payment/微信支付、支付回调、退款、对账、通知留存HTTP notify handler + worker handler
couponinternal/coupon/券模板、用户券、锁定、释放、核销service + admin handler
deliveryinternal/delivery/配送任务、认领、取货、送达、脱敏地址HTTP handler + admin handler
assetinternal/asset/COS 预签名、上传回调、文件元数据、CDN URLHTTP handler
sharedinternal/shared/DB、Valkey、配置、日志、错误码、中间件、outbox、审计公共基础设施

目录规范

每个业务模块保持相同结构,方便 Agent Teams 进入任意模块时快速定位职责。

text
internal/order/
├── handler.go          # Gin handler,只做输入输出和错误映射
├── service.go          # 业务用例和跨模块编排
├── repo.go             # SQL 访问和事务
├── dto.go              # API request/response DTO
├── domain.go           # 领域类型和值对象
├── state_machine.go    # 状态机和合法转换
├── interface.go        # 给其他模块调用的公开接口
└── service_test.go     # 单元测试

依赖方向固定:

Handler 不直接访问数据库;repo 不调用其他模块;跨模块编排只放在 service 层。


模块接口

模块公开能力通过 Go interface 表达。调用方依赖接口,不依赖对方内部 repo 或类型。

go
// internal/inventory/interface.go
package inventory

type Service interface {
    Lock(ctx context.Context, req LockRequest) (*LockResult, error)
    Confirm(ctx context.Context, lockID string) error
    Release(ctx context.Context, lockID string) error
    GetStock(ctx context.Context, productID int64) (*StockInfo, error)
}

type LockItem struct {
    ProductID int64
    BatchNo   string
    Quantity  int // 统一用克或份,按 SKU 定义
}
go
// internal/payment/interface.go
package payment

type Service interface {
    Create(ctx context.Context, req CreatePaymentRequest) (*CreatePaymentResult, error)
    HandleWechatNotify(ctx context.Context, headers http.Header, body []byte) error
    Refund(ctx context.Context, req RefundRequest) (*RefundResult, error)
    Query(ctx context.Context, orderNo string) (*Payment, error)
}
go
// internal/order/interface.go
package order

type Service interface {
    Create(ctx context.Context, req CreateOrderRequest) (*Order, error)
    OnPaymentPaid(ctx context.Context, orderNo string, paymentNo string) error
    ProcessTimeout(ctx context.Context, orderNo string) error
    Cancel(ctx context.Context, req CancelOrderRequest) error
}

下单 Saga

订单创建是同进程 Saga。失败补偿是 Go 函数调用,不涉及网络重试。

go
func (s *OrderService) Create(ctx context.Context, req CreateOrderRequest) (*Order, error) {
    orderNo := s.idgen.NewOrderNo()

    if req.CouponID != 0 {
        if err := s.coupon.Lock(ctx, req.UserID, req.CouponID, orderNo); err != nil {
            return nil, fmt.Errorf("coupon lock: %w", err)
        }
    }

    lock, err := s.inventory.Lock(ctx, toLockRequest(orderNo, req.Items))
    if err != nil {
        _ = s.coupon.Release(ctx, req.CouponID, orderNo)
        return nil, fmt.Errorf("inventory lock: %w", err)
    }

    created, err := s.repo.CreateOrder(ctx, req, orderNo, lock.LockID)
    if err != nil {
        _ = s.inventory.Release(ctx, lock.LockID)
        _ = s.coupon.Release(ctx, req.CouponID, orderNo)
        return nil, fmt.Errorf("create order record: %w", err)
    }

    pay, err := s.payment.Create(ctx, CreatePaymentRequest{
        OrderNo:   orderNo,
        UserID:    req.UserID,
        AmountFen: created.FinalFen,
    })
    if err != nil {
        _ = s.inventory.Release(ctx, lock.LockID)
        _ = s.coupon.Release(ctx, req.CouponID, orderNo)
        _ = s.repo.MarkCancelled(ctx, orderNo, "payment_create_failed")
        return nil, fmt.Errorf("payment create: %w", err)
    }

    if err := s.outbox.Publish(ctx, outbox.Event{
        Type:          "order.timeout",
        AggregateType: "order",
        AggregateID:   orderNo,
        ScheduledAt:   time.Now().Add(30 * time.Minute),
    }); err != nil {
        return nil, fmt.Errorf("publish order timeout: %w", err)
    }

    created.PrepayID = pay.PrepayID
    return created, nil
}

原则:补偿操作必须幂等,补偿失败必须记录日志和 outbox_events 重试任务。


数据 ownership

Owner 模块可读模块写入规则
usersuserauth/order/delivery/admin只有 user 写
user_credentialsauthauth只有 auth 写
user_addressesuserorder/delivery只有 user 写,order 下单时复制快照
productscataloginventory/order/coupon/delivery只有 catalog 写
categoriescatalogcatalog/order只有 catalog 写
trace_recordscatalogcatalog/order/user只有 catalog 写
product_batchesinventorycatalog/order只有 inventory 写
inventoryinventorycatalog/order/admin只有 inventory 写
inventory_locksinventoryorder/worker只有 inventory 写
inventory_movementsinventoryadmin/worker只有 inventory 写
ordersorderpayment/delivery/admin只有 order 写
order_itemsorderdelivery/admin只有 order 写
paymentspaymentorder/admin只有 payment 写
payment_notificationspaymentadmin/worker只有 payment 写
refundspaymentorder/admin只有 payment 写
coupon_templatescouponorder/admin只有 coupon 写
user_couponscouponorder/admin只有 coupon 写
delivery_tasksdeliveryorder/admin只有 delivery 写
assetsassetcatalog/user/admin只有 asset 写
after_salesorderpayment/admin只有 order 写
outbox_eventssharedworker/all modules模块只 insert,worker 更新状态
idempotency_keyssharedall modules幂等中间件写
audit_logssharedadmin/all modules敏感操作必须写

跨模块读可以直接读快照或只读视图;跨模块写必须走 owner service。


Worker 协作

Worker 与 HTTP 进程不通过网络通信。API 模块写 outbox_events,worker 抢任务并调用同仓库模块代码。

Worker 处理规则:

规则说明
抢任务FOR UPDATE SKIP LOCKED,避免多 worker 重复处理
幂等handler 先查当前业务状态,已处理直接返回成功
重试失败后增加 retry_count,写 next_retry_at,指数退避
死信超过 max_retries 标记 failed,触发告警和后台人工处理
日志每个事件必须打印 event_idaggregate_idtrace_id

后置拆分规则

当前路线不拆微服务。如果未来确实要拆,必须先满足这些条件:

  1. 当前模块在单体内已经有稳定 interface 和完整集成测试。
  2. 模块性能瓶颈无法通过 SQL、缓存、垂直扩容或多实例 HTTP 解决。
  3. 有专人负责部署、监控、回滚和事故处理。
  4. 已完成 ADR,说明收益、成本、替代方案、回滚路径。

优先可评估的拆分对象只会是支付、搜索或通知,不会一开始全量拆分。


相关链接

鼎味肉市 · 纯线上猪肉零售