「产品矩阵平台」核心技术架构层

拆解产品矩阵平台核心架构:API Gateway、BFF、Service Layer、Repository、事件驱动、分布式缓存、安全体系与灰度发布完整设计。

第二章 核心技术架构层

产品矩阵平台真正难的地方,不是把几个产品放进同一个代码仓,而是让每个产品都能保持自己的节奏,同时共享一套稳定、可治理、可演进的技术底座。

这一层要回答三个问题:

  1. 请求从用户进入系统后,经过哪些边界?
  2. 业务模块之间如何协作,又如何避免互相污染?
  3. 平台能力如何从同步接口,扩展到异步事件、缓存、权限、灰度和任务调度?

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 只暴露意图明确的方法,而不是把数据库表操作原样暴露出去。例如 CreateOrderInsertOrder 更合适,因为创建订单可能包含库存检查、优惠计算、支付单生成、事件写入等步骤。

推荐的服务方法形态:

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_idcontext.Context 读取租户
软删除统一 deleted_at 策略
审计字段自动写入 created_byupdated_by
数据变更事件谨慎使用,复杂事件优先放 Service

Hook 不适合承载复杂业务规则。比如“支付成功后赠送会员权益”不应该藏在 AfterUpdate 里,否则调用方很难知道一次保存会触发哪些副作用。

2.5 异步与事件驱动架构

平台内的事件应分成两类:

类型例子要求
领域事件order.createduser.registered语义稳定,可被多个模块订阅
集成事件sms.send.requestedfile.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)

服务端校验顺序:

  1. 检查 app_id 是否存在、是否启用;
  2. 检查 timestamp 是否在允许窗口内;
  3. 检查 nonce 是否已使用;
  4. 重新计算签名并常量时间比较;
  5. 写入审计日志。

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)、调度层(分布式任务)。核心价值在于让各产品共享技术底座的同时保持业务自治。


延伸阅读


关联专题

专题关联内容链接
GolangGoravel框架与模块化实践/golang/
TypeScript类型安全与BFF层设计/posts/typescript/
PostgreSQL数据库读写分离与事务/posts/postgresql/
Node.jsBFF层多语言策略/posts/nodejs/
Tailwind CSS管理台UI体系构建/posts/tailwind/

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「SaaS」更多文章

  1. 「产品矩阵平台」未来演进方向
  2. 「产品矩阵平台」运维与成本优化
  3. 「产品矩阵平台」安全与合规体系