第二章 核心技术架构层
产品矩阵平台真正难的地方,不是把几个产品放进同一个代码仓,而是让每个产品都能保持自己的节奏,同时共享一套稳定、可治理、可演进的技术底座。
这一层要回答三个问题:
- 请求从用户进入系统后,经过哪些边界?
- 业务模块之间如何协作,又如何避免互相污染?
- 平台能力如何从同步接口,扩展到异步事件、缓存、权限、灰度和任务调度?
2.1 应用层:API Gateway、BFF 与 GraphQL
应用层是外部世界进入平台的第一道门。对产品矩阵而言,入口不会只有一个:微信小程序、Web 管理台、移动 App、开放 API、第三方 Webhook 都会同时存在。
推荐拆成三类入口:
| 入口 | 主要职责 | 典型场景 |
|---|---|---|
| API Gateway | 鉴权、限流、签名、路由、审计 | 所有外部请求统一入口 |
| BFF | 面向端侧聚合数据,减少前端拼装 | 管理台首页、移动端个人中心 |
| GraphQL | 复杂查询和多资源组合 | 低代码后台、开放查询接口 |
API Gateway 不应该承载业务逻辑。它更像机场安检:检查身份、行李和登机口,但不负责飞机怎么飞。真正的业务决策应下沉到服务层,否则网关会很快变成“第二个单体”。
一个可落地的请求链路如下:
sequenceDiagram
participant Client as Client
participant Gateway as API Gateway
participant BFF as BFF
participant Service as Service Layer
participant Repo as Repository
Client->>Gateway: Request + JWT + X-App-ID
Gateway->>Gateway: Auth / Rate Limit / Signature
Gateway->>BFF: Forward normalized request
BFF->>Service: Call domain services
Service->>Repo: Query / Command
Repo-->>Service: Domain data
Service-->>BFF: Business result
BFF-->>Client: View model
2.2 业务服务层:模块化 Service Layer
Service Layer 是平台业务能力的主战场。一个成熟平台至少要把服务分成三类:
| 服务类型 | 示例 | 设计重点 |
|---|---|---|
| 基础服务 | 用户、租户、认证、文件 | 稳定、强一致、低变更 |
| 业务服务 | 订单、内容、表单、任务 | 高内聚、边界清晰 |
| 平台服务 | 配置、审计、计费、通知 | 跨产品复用、规则可配置 |
Service 只暴露意图明确的方法,而不是把数据库表操作原样暴露出去。例如 CreateOrder 比 InsertOrder 更合适,因为创建订单可能包含库存检查、优惠计算、支付单生成、事件写入等步骤。
推荐的服务方法形态:
type OrderService interface {
CreateOrder(ctx context.Context, cmd CreateOrderCommand) (*OrderDTO, error)
CancelOrder(ctx context.Context, cmd CancelOrderCommand) error
}
这里的 Command 是业务输入,不是 HTTP 请求体的直接复用。这样做可以避免 Service 被某一个端的字段设计绑死。
2.3 多租户与多应用上下文
产品矩阵平台中,每一次请求都至少有三层身份:
| 上下文 | 含义 | 来源 |
|---|---|---|
| User | 当前登录用户 | JWT、Session、OAuth |
| Tenant | 企业、品牌、开发者主体 | 域名、AppID、JWT Claims |
| App | 具体产品或应用实例 | Header、路径、客户端配置 |
上下文必须在网关或中间件层被解析,然后通过 context.Context 进入 Service 和 Repository。不要在业务代码里反复读取 Header,因为那会让业务逻辑依赖 HTTP 环境,未来迁移到 Worker、CLI 或消息消费时会很痛苦。
2.4 数据访问层:GORM、Repository 与 Hook
Repository 的职责不是“包一层 ORM”,而是稳定数据访问契约。业务层不应该知道某个查询用了联表、索引、缓存还是读库。
推荐边界:
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
FindByUnionID(ctx context.Context, tenantID string, unionID string) (*User, error)
Save(ctx context.Context, user *User) error
}
GORM Hook 适合处理横切能力:
| Hook 场景 | 建议做法 |
|---|---|
自动写入 tenant_id | 从 context.Context 读取租户 |
| 软删除 | 统一 deleted_at 策略 |
| 审计字段 | 自动写入 created_by、updated_by |
| 数据变更事件 | 谨慎使用,复杂事件优先放 Service |
Hook 不适合承载复杂业务规则。比如“支付成功后赠送会员权益”不应该藏在 AfterUpdate 里,否则调用方很难知道一次保存会触发哪些副作用。
2.5 异步与事件驱动架构
平台内的事件应分成两类:
| 类型 | 例子 | 要求 |
|---|---|---|
| 领域事件 | order.created、user.registered | 语义稳定,可被多个模块订阅 |
| 集成事件 | sms.send.requested、file.thumbnail.required | 面向外部系统或异步任务 |
最容易踩坑的是:数据库事务提交成功了,消息发送失败了。建议使用 Outbox Pattern,在业务事务内先写入 outbox_events 表,再由独立 Worker 投递到 Kafka、NATS 或 Redis Stream。
业务事务:写订单 + 写 outbox_events
后台任务:扫描未投递事件 → 发布消息 → 标记已投递
消费端:按 event_id 幂等处理
这样即使消息系统短暂不可用,平台也不会丢失关键事件。
2.6 缓存与分布式锁
缓存的第一原则是:先定义一致性要求,再选择技术方案。
| 数据类型 | 推荐策略 | 例子 |
|---|---|---|
| 读多写少 | Cache Aside + TTL | 商品详情、文章详情 |
| 强配置读取 | 本地缓存 + Redis 发布订阅 | Feature Flag |
| 高频计数 | Redis 原子操作 + 异步落库 | 点赞数、浏览量 |
| 临界资源 | 分布式锁 + 超时保护 | 库存扣减、账单生成 |
分布式锁必须有过期时间、唯一 token 和释放校验。不能简单地 DEL lock_key,因为可能误删别人刚拿到的锁。
2.7 接口安全与签名体系
开放平台接口要同时处理三件事:身份可信、内容未被篡改、请求不可重放。
一个实用的签名规则:
sign_string = method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body)
signature = HMAC-SHA256(app_secret, sign_string)
服务端校验顺序:
- 检查
app_id是否存在、是否启用; - 检查
timestamp是否在允许窗口内; - 检查
nonce是否已使用; - 重新计算签名并常量时间比较;
- 写入审计日志。
2.8 灰度发布与版本控制
产品矩阵会同时服务多个 App 和租户,不能把“发布代码”当成“发布功能”。Feature Toggle 是必需品。
| 灰度维度 | 示例 |
|---|---|
| 租户 | 只给某个企业开启新版报表 |
| App | 只给小程序端开启新结算页 |
| 用户 | 按用户 ID 尾号抽样 5% |
| 环境 | dev、staging、prod 分层 |
灰度配置要有版本号、变更人、发布时间和回滚记录。否则一次开关变更很难追踪事故来源。
2.9 分布式任务调度与工作流
平台任务通常分为定时任务、延迟任务、批处理任务和长流程任务。不要把它们都塞进一个 Cron。
| 任务类型 | 例子 | 推荐实现 |
|---|---|---|
| 定时任务 | 每日账单、数据归档 | Scheduler + 分布式锁 |
| 延迟任务 | 订单超时取消 | Redis ZSet / Delay Queue |
| 批处理 | 用户画像重算 | Worker Pool |
| 工作流 | 企业入驻审核 | 状态机 + 事件 |
工作流设计的关键是可恢复。每一步都应记录状态和输入输出,即使 Worker 重启,也能从最后一个成功节点继续执行。
2.10 本章落地清单
| 检查项 | 达标标准 |
|---|---|
| 入口层 | 网关只做通用控制,BFF 只做端侧聚合 |
| 服务层 | 服务接口表达业务意图,不泄露 HTTP 和 ORM |
| 数据层 | Repository 明确,Hook 只处理横切能力 |
| 事件层 | 关键事件使用 Outbox,消费者幂等 |
| 缓存层 | 每类缓存都有一致性策略和失效策略 |
| 安全层 | JWT、HMAC、OAuth2 各有边界 |
| 灰度层 | 配置变更可追踪、可回滚 |
| 调度层 | 任务可重试、可恢复、可观测 |
2.11 代码实践:Gateway 中间件链与灰度判定
一、API Gateway 中间件链
package gateway
import (
"context"
"net/http"
"strings"
"time"
)
// GatewayMiddleware 按顺序执行:身份校验 -> 签名校验 -> 限流 -> 路由
func GatewayMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 1. JWT 身份校验
user, err := authenticateJWT(r)
if err != nil {
respondJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return
}
ctx = context.WithValue(ctx, "user", user)
// 2. API 签名校验(开放平台)
if r.Header.Get("X-Signature") != "" {
if err := verifySignature(r); err != nil {
respondJSON(w, http.StatusForbidden, map[string]string{"error": "invalid signature"})
return
}
}
// 3. 租户上下文注入
tenant := resolveTenant(r)
ctx = context.WithValue(ctx, "tenant", tenant)
// 4. 限流
if !RateLimiter.Allow(ctx, tenant.ID, r.URL.Path) {
respondJSON(w, http.StatusTooManyRequests, map[string]string{"error": "rate limited"})
return
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// verifySignature HMAC-SHA256 签名
func verifySignature(r *http.Request) error {
appID := r.Header.Get("X-App-ID")
timestamp := r.Header.Get("X-Timestamp")
nonce := r.Header.Get("X-Nonce")
sig := r.Header.Get("X-Signature")
secret := MustGetAppSecret(appID)
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(body))
strToSign := r.Method + "\n" + r.URL.Path + "\n" + timestamp + "\n" + nonce + "\n" + sha256Hex(body)
expected := hmacSHA256(secret, strToSign)
if subtle.ConstantTimeCompare([]byte(sig), []byte(expected)) != 1 {
return fmt.Errorf("signature mismatch")
}
// 防重放
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return fmt.Errorf("request expired")
}
return nil
}
二、Repository 跨库读写分离
package repository
type UserRepository struct {
writer *gorm.DB // 主库
reader *gorm.DB // 从库
}
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
var user User
err := r.reader.WithContext(ctx).First(&user, "id = ?", id).Error
return &user, err
}
func (r *UserRepository) Save(ctx context.Context, user *User) error {
return r.writer.WithContext(ctx).Save(user).Error
}
func (r *UserRepository) FindByUnionID(ctx context.Context, tenantID, unionID string) (*User, error) {
var user User
err := r.reader.WithContext(ctx).
Joins("LEFT JOIN identities ON identities.account_id = users.id").
Where("users.tenant_id = ? AND identities.provider_id = ?", tenantID, unionID).
First(&user).Error
return &user, err
}
// 对于强一致读取(写入后立即读取),强制走主库
func (r *UserRepository) FindByIDStrong(ctx context.Context, id string) (*User, error) {
var user User
err := r.writer.WithContext(ctx).First(&user, "id = ?", id).Error
return &user, err
}
三、Feature Toggle(灰度判定)
package feature
import (
"context"
"hash/fnv"
"time"
)
type Toggle struct {
Key string `json:"key"`
Enabled bool `json:"enabled"`
Target string `json:"target"` // all / tenant / app / user / rollout
TargetID string `json:"target_id"` // 目标标识,如租户ID
Percentage int `json:"percentage"` // 0-100,rollout 时使用
StartAt *time.Time `json:"start_at"`
EndAt *time.Time `json:"end_at"`
}
func (t *Toggle) Allow(ctx context.Context, tenantID, appID, userID string) bool {
if !t.Enabled {
return false
}
now := time.Now()
if t.StartAt != nil && now.Before(*t.StartAt) {
return false
}
if t.EndAt != nil && now.After(*t.EndAt) {
return false
}
switch t.Target {
case "all":
return true
case "tenant":
return tenantID == t.TargetID
case "app":
return appID == t.TargetID
case "user":
return userID == t.TargetID
case "rollout":
return hashInRange(userID, t.Percentage)
}
return false
}
// 按 userID 哈希决定是否命中灰度
func hashInRange(userID string, percentage int) bool {
h := fnv.New32a()
h.Write([]byte(userID))
return int(h.Sum32()%100) < percentage
}
// 使用示例:决策是否启用新版结算页
func (svc *OrderService) CheckoutPage(ctx context.Context, userID string) (string, error) {
toggle := svc.cfg.GetToggle("new_checkout_v2")
if toggle.Allow(ctx, svc.tenantID, svc.appID, userID) {
return svc.newCheckoutPage(ctx)
}
return svc.legacyCheckoutPage(ctx)
}
四、分布式任务调度框架
package scheduler
import (
"context"
"fmt"
"sync"
"time"
)
type Scheduler struct {
tasks map[string]*Task
mu sync.RWMutex
leader *leader.Election
stopCh chan struct{}
wg sync.WaitGroup
}
type Task struct {
ID string
Interval time.Duration
Handler func(ctx context.Context) error
}
func (s *Scheduler) Register(t *Task) {
s.mu.Lock()
defer s.mu.Unlock()
s.tasks[t.ID] = t
}
func (s *Scheduler) Start() {
for _, t := range s.tasks {
s.wg.Add(1)
go s.runTask(t)
}
}
func (s *Scheduler) runTask(t *Task) {
defer s.wg.Done()
ticker := time.NewTicker(t.Interval)
defer ticker.Stop()
for {
select {
case <-s.stopCh:
return
case <-ticker.C:
if s.leader != nil && !s.leader.IsLeader() {
continue // 非 Leader 不执行
}
ctx, cancel := context.WithTimeout(context.Background(), t.Interval)
if err := t.Handler(ctx); err != nil {
// 记录失败日志,支持重试计数与退避
fmt.Printf("task %s failed: %v\n", t.ID, err)
}
cancel()
}
}
}
func (s *Scheduler) Stop() {
close(s.stopCh)
s.wg.Wait()
}
五、GORM 软删除与审计 Hook
package hooks
import (
"context"
"time"
"gorm.io/gorm"
)
func RegisterAuditHooks(db *gorm.DB) {
// 自动写入 tenant_id
db.Callback().Create().Before("gorm:create").Register("auto_tenant", func(db *gorm.DB) {
if tenant := GetTenant(db.Statement.Context); tenant != nil {
if field := db.Statement.Schema.LookUpField("TenantID"); field != nil {
_ = db.Statement.SetColumn("TenantID", tenant.ID)
}
if field := db.Statement.Schema.LookUpField("AppID"); field != nil {
_ = db.Statement.SetColumn("AppID", tenant.AppID)
}
}
})
// 审计字段
now := time.Now()
db.Callback().Create().Before("gorm:create").Register("auto_created_at", func(db *gorm.DB) {
if field := db.Statement.Schema.LookUpField("CreatedAt"); field != nil {
_ = db.Statement.SetColumn("CreatedAt", now)
}
if user := GetUserID(db.Statement.Context); user != "" {
if field := db.Statement.Schema.LookUpField("CreatedBy"); field != nil {
_ = db.Statement.SetColumn("CreatedBy", user)
}
}
})
db.Callback().Update().Before("gorm:update").Register("auto_updated_at", func(db *gorm.DB) {
if field := db.Statement.Schema.LookUpField("UpdatedAt"); field != nil {
_ = db.Statement.SetColumn("UpdatedAt", now)
}
})
// 禁止物理删除,必须通过软删除
db.Callback().Delete().Before("gorm:delete").Register("block_hard_delete", func(db *gorm.DB) {
if db.Statement.Unscoped {
return // Allow Unscoped() bypass
}
// 默认转换为软删除
db.Statement.SetColumn("DeletedAt", now)
db.Statement.SQL.Grow(100)
})
}
本章小结
本章拆解了产品矩阵平台核心技术架构层的八个维度:入口层(Gateway/BFF)、服务层(模块化 Service)、数据层(GORM Hook + Repository)、事件层(Outbox 模式)、缓存层(多级缓存与一致性)、安全层(JWT/HMAC/OAuth2)、灰度层(Feature Toggle)、调度层(分布式任务)。核心价值在于让各产品共享技术底座的同时保持业务自治。
延伸阅读
- 上一章:总体架构蓝图 — 平台化战略、分层架构模型与技术栈选型方法论。
- 下一章:平台通用能力中心 — 用户中心、配置中心、文件中心、通知中心等公共能力的沉淀方案。
关联专题
| 专题 | 关联内容 | 链接 |
|---|---|---|
| Golang | Goravel框架与模块化实践 | /golang/ |
| TypeScript | 类型安全与BFF层设计 | /posts/typescript/ |
| PostgreSQL | 数据库读写分离与事务 | /posts/postgresql/ |
| Node.js | BFF层多语言策略 | /posts/nodejs/ |
| Tailwind CSS | 管理台UI体系构建 | /posts/tailwind/ |
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。