模块架构
Go 模块化单体 · 同进程 interface 调用 · 清晰数据 ownership · worker 只通过数据库事件协作
核心理念
后端不拆成多个 K8s 微服务。api-server、admin-api、worker 是三个 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 │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘模块之间不是网络边界,而是代码边界。这样可以保留拆分时需要的接口纪律,同时避免微服务带来的部署、排障、事务和上下文成本。
模块清单
| 模块 | 目录 | 职责 | 主要入口 |
|---|---|---|---|
| auth | internal/auth/ | 微信登录、密码登录、JWT、token version、后台账号权限 | HTTP handler + service |
| user | internal/user/ | 用户资料、收货地址、会员积分 | HTTP handler + service |
| catalog | internal/catalog/ | 商品、分类、溯源、商品图引用、基础搜索 | HTTP handler + admin handler |
| inventory | internal/inventory/ | 批次库存、锁定、释放、扣减、库存流水 | service + admin handler |
| order | internal/order/ | 订单状态机、计价、订单项、售后入口、Saga 编排 | HTTP handler + worker handler |
| payment | internal/payment/ | 微信支付、支付回调、退款、对账、通知留存 | HTTP notify handler + worker handler |
| coupon | internal/coupon/ | 券模板、用户券、锁定、释放、核销 | service + admin handler |
| delivery | internal/delivery/ | 配送任务、认领、取货、送达、脱敏地址 | HTTP handler + admin handler |
| asset | internal/asset/ | COS 预签名、上传回调、文件元数据、CDN URL | HTTP handler |
| shared | internal/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 模块 | 可读模块 | 写入规则 |
|---|---|---|---|
users | user | auth/order/delivery/admin | 只有 user 写 |
user_credentials | auth | auth | 只有 auth 写 |
user_addresses | user | order/delivery | 只有 user 写,order 下单时复制快照 |
products | catalog | inventory/order/coupon/delivery | 只有 catalog 写 |
categories | catalog | catalog/order | 只有 catalog 写 |
trace_records | catalog | catalog/order/user | 只有 catalog 写 |
product_batches | inventory | catalog/order | 只有 inventory 写 |
inventory | inventory | catalog/order/admin | 只有 inventory 写 |
inventory_locks | inventory | order/worker | 只有 inventory 写 |
inventory_movements | inventory | admin/worker | 只有 inventory 写 |
orders | order | payment/delivery/admin | 只有 order 写 |
order_items | order | delivery/admin | 只有 order 写 |
payments | payment | order/admin | 只有 payment 写 |
payment_notifications | payment | admin/worker | 只有 payment 写 |
refunds | payment | order/admin | 只有 payment 写 |
coupon_templates | coupon | order/admin | 只有 coupon 写 |
user_coupons | coupon | order/admin | 只有 coupon 写 |
delivery_tasks | delivery | order/admin | 只有 delivery 写 |
assets | asset | catalog/user/admin | 只有 asset 写 |
after_sales | order | payment/admin | 只有 order 写 |
outbox_events | shared | worker/all modules | 模块只 insert,worker 更新状态 |
idempotency_keys | shared | all modules | 幂等中间件写 |
audit_logs | shared | admin/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_id、aggregate_id、trace_id |
后置拆分规则
当前路线不拆微服务。如果未来确实要拆,必须先满足这些条件:
- 当前模块在单体内已经有稳定 interface 和完整集成测试。
- 模块性能瓶颈无法通过 SQL、缓存、垂直扩容或多实例 HTTP 解决。
- 有专人负责部署、监控、回滚和事故处理。
- 已完成 ADR,说明收益、成本、替代方案、回滚路径。
优先可评估的拆分对象只会是支付、搜索或通知,不会一开始全量拆分。