导航书籍主页 | 完整目录 | 上一章 | 下一章


第16章 B2B2C平台完整架构

综合案例:一个中大型B2B2C电商平台的完整架构设计,从品类分析到技术选型,从系统设计到团队协作,覆盖200+人团队、日订单200万级的实战经验与架构决策。


16.1 项目背景

16.1.1 业务模式

核心定位:B2B2C聚合电商平台

本平台采用"聚合供应商"模式,连接50+外部供应商(机票、酒店、充值、电影票等),同时支持自营业务(优惠券、礼品卡)。关键特征是「无物流场景」——所有商品均为虚拟数字商品,履约通过API调用完成,无需物流配送环节。

业务范围

  • B2B2C聚合模式:对接机票(航司/GDS)、酒店(OTA/PMS)、充值(运营商)、电影票(院线)等50+供应商
  • 自营模式:自有优惠券(e-voucher)、线下券、礼品卡等虚拟商品
  • 数字履约:所有商品通过API调用完成履约(出票、确认、充值、发券码)

业务特点

  • 供应商接口高度碎片化(实时查询 + 定时同步 + 推送混合)
  • 核心品类(机票/酒店)零超卖容忍——直接影响用户体验与平台信誉
  • 长尾品类(充值/礼品卡)可事后补偿——供应商侧库存无限

16.1.2 团队规模

团队组成(200+人):

团队人数职责
前台团队60人搜索、详情、购物车、结算、订单、用户中心
中台团队80人商品中心、库存、计价、营销、支付、供应商网关
基础设施30人DevOps、监控、中间件、数据库、安全
数据团队20人数据仓库、BI、用户画像、推荐算法
测试团队10人自动化测试、性能测试、安全测试

技术栈统一

  • 后端语言:Go(统一技术栈,降低维护成本)
  • 数据库:MySQL(主库)+ Redis(缓存)+ Elasticsearch(搜索)
  • 消息队列:Kafka(异步解耦)
  • 服务治理:gRPC + Consul + Envoy
  • 监控:Prometheus + Grafana + Jaeger

16.1.3 技术目标

性能指标

指标正常值大促峰值说明
日订单量200万1000万大促5倍流量
搜索QPS3000150005倍流量
详情页QPS5000250005倍流量
下单QPS100050005倍流量
P99延迟200ms500ms大促允许适当降级

可用性目标

  • 核心链路SLA:99.95%(订单创建、支付、履约)
  • 搜索/详情SLA:99.9%(可降级展示)
  • RTO(故障恢复时间):< 5分钟
  • RPO(数据丢失容忍):0(核心交易数据零丢失)

扩展性目标

  • 支持新品类接入:< 2周(标准化供应商适配)
  • 支持新供应商:< 1周(适配器模式)
  • 支持新营销玩法:< 3天(规则引擎)

16.2 品类业务模型分析

不同品类的业务模型存在显著差异,直接影响架构设计决策。理解这些差异是系统设计的基础。

16.2.1 机票业务模型

业务特点

• 库存模型:实时库存(供应商侧),强依赖供应商实时查询
• 价格模型:动态定价,实时波动(可能秒级变化)
• SKU复杂度:极高(航司+航班号+舱位+日期+...组合)
• 库存单位:座位数量(不可超卖)
• 扣减时机:下单即扣(预占)→ 支付确认 → 出票
• 履约流程:下单 → 支付 → 出票(调用GDS/供应商API)→ 发送电子票

架构影响

  • ✓ 必须支持实时库存查询(高频调用供应商API)
  • ✓ 价格快照必须精确到秒级,防止价格变动纠纷
  • ✓ 超卖零容忍 → 下单前二次确认库存
  • ✓ 供应商故障需快速切换到备用供应商
  • ✓ 订单状态复杂(待出票、出票中、出票失败、已出票)

技术要点

// 机票库存查询策略
type FlightStockStrategy struct {
    supplierClient rpc.SupplierClient
    redis          redis.Client
    config         *FlightConfig
}

func (s *FlightStockStrategy) CheckStock(ctx context.Context, req *StockRequest) (*StockResponse, error) {
    // Step 1: 尝试从Redis获取缓存(TTL=5分钟)
    cacheKey := fmt.Sprintf("flight:stock:%s:%s", req.FlightNo, req.Date)
    cached, err := s.redis.Get(ctx, cacheKey).Result()
    if err == nil {
        return parseStockFromCache(cached), nil
    }
    
    // Step 2: 缓存未命中,调用供应商实时查询
    ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)  // 800ms超时
    defer cancel()
    
    stock, err := s.supplierClient.QueryStock(ctx, req)
    if err != nil {
        // 供应商故障,切换备用供应商
        return s.fallbackToSecondarySupplier(ctx, req)
    }
    
    // Step 3: 缓存结果(短TTL,机票价格变化快)
    s.redis.Set(ctx, cacheKey, marshal(stock), 5*time.Minute)
    
    return stock, nil
}

监控指标

  • 供应商调用超时率:< 1%
  • 缓存命中率:> 70%
  • 出票成功率:> 99.5%
  • 出票平均时长:< 30秒

16.2.2 酒店业务模型

业务特点

• 库存模型:房间数量(按日期维度管理)
• 价格模型:日历房价(每个日期不同价格)
• SKU复杂度:高(酒店ID+房型+日期范围+早餐+...)
• 库存单位:房间数/间夜数
• 扣减时机:下单预占 → 支付确认 → 供应商确认
• 履约流程:下单 → 支付 → 提交供应商 → 确认单 → 入住凭证

架构影响

  • ✓ 支持日期范围查询(check-in到check-out)
  • ✓ 日历价格存储(每个日期一条记录)
  • ✓ 库存按日期维度管理(某天无房不影响其他日期)
  • ✓ 支持"担保"模式(先占房,入住时结算)
  • ✓ 需处理"确认单延迟"(供应商异步确认)

数据模型

// 酒店日历价格表(宽表存储)
type HotelCalendarPrice struct {
    HotelID      int64     `gorm:"primaryKey"`
    RoomTypeID   int64     `gorm:"primaryKey"`
    Date         time.Time `gorm:"primaryKey;index"`  // 日期维度
    BasePrice    int64     // 基础价格(分)
    WeekendPrice int64     // 周末价格
    Stock        int       // 当日库存
    Status       string    // 可售状态(AVAILABLE/SOLD_OUT/CLOSED)
}

// 查询日期范围内的价格与库存
func (r *HotelRepo) GetCalendarPrice(hotelID, roomTypeID int64, checkIn, checkOut time.Time) ([]*HotelCalendarPrice, error) {
    var prices []*HotelCalendarPrice
    err := r.db.Where("hotel_id = ? AND room_type_id = ? AND date >= ? AND date < ?",
        hotelID, roomTypeID, checkIn, checkOut).
        Order("date ASC").
        Find(&prices).Error
    return prices, err
}

缓存策略

  • 热门酒店:30分钟缓存
  • 长尾酒店:1小时缓存
  • 价格变更:主动失效缓存

16.2.3 充值业务模型

业务特点

• 库存模型:无限库存(供应商侧无限制)
• 价格模型:固定面额(10元、50元、100元)
• SKU复杂度:低(运营商+面额)
• 库存单位:无限
• 扣减时机:支付后
• 履约流程:下单 → 支付 → 调用供应商API → 充值成功/失败

架构影响

  • ✓ 无需库存管理(库存类型=无限)
  • ✓ 价格简单(基础价+平台服务费)
  • ✓ 超卖可接受(事后补偿)
  • ✓ 供应商调用简单(同步API,3秒内返回)
  • ✓ 失败重试友好(幂等性强)

技术要点

// 充值库存策略(无限库存)
type RechargeStockStrategy struct{}

func (s *RechargeStockStrategy) CheckStock(ctx context.Context, req *StockRequest) (*StockResponse, error) {
    // 充值类商品无需检查库存,直接返回"可售"
    return &StockResponse{
        Available: true,
        Quantity:  999999,  // 虚拟无限库存
        Message:   "充值类商品,库存充足",
    }, nil
}

func (s *RechargeStockStrategy) Reserve(ctx context.Context, req *ReserveRequest) (*ReserveResponse, error) {
    // 充值类商品无需预占,直接返回成功
    return &ReserveResponse{
        ReserveID: "",  // 无预占ID
        Success:   true,
    }, nil
}

16.2.4 电子券业务模型

业务特点

• 库存模型:固定库存(券码池)
• 价格模型:固定折扣价
• SKU复杂度:中(商户+门店+商品+...)
• 库存单位:券码(一券一码)
• 扣减时机:支付后
• 履约流程:下单 → 支付 → 发券码 → 到店核销

架构影响

  • ✓ 券码池管理(预生成10万个券码)
  • ✓ 券码发放(支付后随机分配)
  • ✓ 核销系统(商户扫码核销)
  • ✓ 过期管理(券有效期7天-180天)
  • ✓ 退款逻辑(未核销可退,已核销不可退)

技术要点

// 券码池管理(Redis实现)
type VoucherCodePool struct {
    redis redis.Client
}

func (p *VoucherCodePool) AssignCode(ctx context.Context, skuID int64, orderID int64) (string, error) {
    // Step 1: 从Redis Set中原子弹出一个未使用的券码
    poolKey := fmt.Sprintf("voucher:pool:%d", skuID)
    code, err := p.redis.SPop(ctx, poolKey).Result()
    if err == redis.Nil {
        return "", errors.New("券码已售罄")
    }
    
    // Step 2: 记录券码分配关系(券码 → 订单号)
    assignKey := fmt.Sprintf("voucher:assign:%s", code)
    p.redis.Set(ctx, assignKey, orderID, 0)  // 永久存储
    
    // Step 3: 设置券码有效期(ZSet按过期时间排序)
    expiresAt := time.Now().Add(90 * 24 * time.Hour)  // 90天有效期
    expiryKey := fmt.Sprintf("voucher:expiry:%d", skuID)
    p.redis.ZAdd(ctx, expiryKey, redis.Z{
        Score:  float64(expiresAt.Unix()),
        Member: code,
    })
    
    return code, nil
}

16.2.5 差异化设计策略

通过上述品类分析,我们提炼出三个核心设计维度:

维度1:库存管理类型

类型典型品类库存来源预占策略
实时库存机票、酒店、电影票供应商实时查询下单即预占,超时释放
池化库存优惠券、礼品卡平台自有(券码池)支付后扣减
无限库存充值、SaaS服务无库存概念无需预占

维度2:价格模型

类型典型品类缓存策略快照策略
动态定价机票5分钟TTL秒级快照
日历定价酒店30分钟TTL日期维度快照
固定定价充值、礼品卡1小时TTL简单快照

维度3:履约模式

类型典型品类调用方式失败处理
同步履约充值同步API(3秒超时)立即重试3次
异步履约机票、酒店异步轮询(30秒/次)补偿任务
券码发放优惠券本地分配(无外部调用)券码池补充

统一抽象

// 品类策略接口(策略模式)
type CategoryStrategy interface {
    // 库存检查
    CheckStock(ctx context.Context, req *StockRequest) (*StockResponse, error)
    // 库存预占
    ReserveStock(ctx context.Context, req *ReserveRequest) (*ReserveResponse, error)
    // 价格计算
    CalculatePrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error)
    // 订单履约
    Fulfill(ctx context.Context, order *Order) (*FulfillResult, error)
}

// 策略工厂(根据品类选择策略)
type CategoryStrategyFactory struct {
    strategies map[CategoryType]CategoryStrategy
}

func (f *CategoryStrategyFactory) GetStrategy(categoryType CategoryType) CategoryStrategy {
    return f.strategies[categoryType]
}

设计原则

  1. 策略模式:每个品类一个策略实现,避免 if-else 地狱
  2. 适配器模式:统一供应商接口差异,降低耦合
  3. 模板方法:下单流程统一,具体步骤由策略实现
  4. 可扩展性:新增品类只需新增策略,不影响主流程

16.3 整体架构设计

16.3.1 分层架构

采用经典的四层架构,确保职责清晰、易于维护。

┌──────────────────────────────────────────────────────┐
│              接入层(API Gateway)                    │
│  • 鉴权、限流、路由、协议转换                         │
│  • Web/App/小程序统一接入                            │
└──────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────┐
│             聚合层(Aggregation Service)             │
│  • 数据编排:并发调用多个微服务                       │
│  • 降级策略:服务故障时的降级处理                     │
│  • 缓存优化:聚合结果缓存                            │
└──────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│                   业务服务层(Microservices)                │
│  ┌────────┬────────┬────────┬────────┬────────┬────────┐   │
│  │ Product│Inventory│ Pricing│Marketing│ Order │ Payment│   │
│  │  商品  │  库存  │  计价  │  营销  │  订单 │  支付  │   │
│  └────────┴────────┴────────┴────────┴────────┴────────┘   │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────┐
│           基础设施层(Infrastructure)                │
│  • MySQL、Redis、Elasticsearch、Kafka               │
│  • 服务发现(Consul)、服务网格(Envoy)             │
│  • 监控告警(Prometheus、Grafana、Jaeger)          │
└──────────────────────────────────────────────────────┘

分层职责

层级服务职责不负责
接入层API Gateway鉴权、限流、路由业务逻辑、数据编排
聚合层Aggregation数据获取、编排、降级具体业务计算
业务层Microservices单一业务领域逻辑跨域数据获取
基础层Infra存储、消息、监控业务规则

16.3.2 微服务拆分

拆分原则

  1. 按业务能力拆分(而非技术层次)
  2. 单一职责:每个服务只负责一个限界上下文
  3. 数据所有权:每个服务拥有自己的数据库
  4. API优先:服务间只通过API或事件通信

核心服务清单

服务名称职责数据库QPS(峰值)团队规模
Product Center商品信息、类目、属性MySQL(4分库)2000012人
Inventory Service库存管理、预占、扣减MySQL+Redis800010人
Pricing Service价格计算、试算、快照MySQL150008人
Marketing Service营销规则、优惠券、活动MySQL+Redis1000012人
Order Service订单创建、状态机、履约MySQL(8分库64表)500015人
Payment Service支付、退款、对账MySQL600010人
Search Service商品搜索、筛选、排序Elasticsearch150008人
User Service用户信息、登录、权限MySQL80006人
Supplier Gateway供应商对接、适配、熔断MySQL+Redis1200015人

聚合服务

服务职责依赖服务
Search Aggregation搜索结果聚合Search + Product + Inventory + Pricing
Detail Aggregation详情页聚合Product + Inventory + Pricing + Marketing
Checkout Aggregation结算页聚合Product + Inventory + Pricing + Marketing

16.3.3 服务依赖关系

graph TB
    subgraph 接入层
        Gateway[API Gateway]
    end
    
    subgraph 聚合层
        SearchAgg[搜索聚合]
        DetailAgg[详情聚合]
        CheckoutAgg[结算聚合]
    end
    
    subgraph 业务服务层
        Product[商品中心]
        Inventory[库存服务]
        Pricing[计价服务]
        Marketing[营销服务]
        Order[订单服务]
        Payment[支付服务]
        Search[搜索服务]
    end
    
    subgraph 基础服务
        Supplier[供应商网关]
        User[用户服务]
    end
    
    Gateway --> SearchAgg
    Gateway --> DetailAgg
    Gateway --> CheckoutAgg
    Gateway --> Order
    
    SearchAgg --> Search
    SearchAgg --> Product
    SearchAgg --> Inventory
    SearchAgg --> Pricing
    
    DetailAgg --> Product
    DetailAgg --> Inventory
    DetailAgg --> Pricing
    DetailAgg --> Marketing
    
    CheckoutAgg --> Product
    CheckoutAgg --> Inventory
    CheckoutAgg --> Pricing
    CheckoutAgg --> Marketing
    
    Order --> Inventory
    Order --> Payment
    Order --> Supplier
    
    Inventory --> Supplier
    Product --> Supplier

依赖原则

  1. 上游 → 下游:聚合层调用业务层,不反向依赖
  2. 避免循环依赖:严格禁止服务间循环调用
  3. 异步解耦:非核心路径使用Kafka事件异步
  4. 降级友好:下游故障不影响上游核心功能

16.3.4 数据流转

同步数据流(关键路径)

用户搜索商品:
API Gateway → Search Aggregation 
            → Search Service(ES查询)
            → Product Service(批量获取基础信息)
            → Inventory Service(批量查库存)
            → Pricing Service(批量计算价格)
            ← 返回聚合结果

响应时间:< 200ms(P99)

异步数据流(非关键路径)

订单创建成功 → Kafka Event:OrderCreated
            → 订阅者1:Inventory Service(确认扣减)
            → 订阅者2:Search Service(更新销量)
            → 订阅者3:User Service(积分增加)
            → 订阅者4:Data Team(数据分析)

最终一致性:< 5秒

16.4 技术选型决策

16.4.1 选型原则

原则1:成熟度优先

  • 优先选择生产级成熟技术(避免踩坑)
  • 社区活跃、文档完善、案例丰富
  • 避免使用 alpha/beta 版本

原则2:团队能力匹配

  • 技术栈与团队技能对齐
  • 学习曲线可控(新技术培训 < 1个月)
  • 有内部专家支持

原则3:生态完整性

  • 工具链完善(测试、监控、部署)
  • 第三方库丰富
  • 云服务支持(AWS/GCP/阿里云)

原则4:成本可控

  • 开源优先(降低License成本)
  • 云服务按需使用(避免自建中间件)
  • 运维成本可接受

16.4.2 Go生态选型

语言选择:Go

维度GoJava理由
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐协程模型,高并发性能优异
开发效率⭐⭐⭐⭐⭐⭐⭐编译快,部署简单(单一二进制)
学习曲线⭐⭐⭐⭐⭐⭐⭐⭐语法简洁,容易上手
生态⭐⭐⭐⭐⭐⭐⭐⭐⭐微服务生态完善(gRPC/Consul/Envoy)
团队能力⭐⭐⭐⭐⭐⭐⭐⭐团队有Go经验

Web框架:Gin

// 理由:
// 1. 性能优异(httprouter,零内存分配)
// 2. 中间件丰富(鉴权、限流、日志)
// 3. 社区活跃(GitHub 70k+ stars)

router := gin.Default()
router.Use(middleware.Auth())
router.Use(middleware.RateLimit(1000))
router.GET("/products/:id", handler.GetProduct)

ORM:GORM

// 理由:
// 1. 支持MySQL、PostgreSQL、SQLite
// 2. 关联查询、预加载、Hook机制完善
// 3. 自动迁移(开发环境)

type Product struct {
    ID       int64  `gorm:"primaryKey"`
    Title    string `gorm:"size:255;not null"`
    Price    int64  `gorm:"not null"`
}

RPC:gRPC + Protobuf

// 理由:
// 1. 二进制序列化(性能优于JSON)
// 2. 强类型(编译期检查)
// 3. 支持流式调用(双向流)

service ProductService {
    rpc GetProduct(GetProductRequest) returns (GetProductResponse);
    rpc BatchGetProduct(BatchGetProductRequest) returns (stream Product);
}

依赖注入:Google Wire

// 理由:
// 1. 编译时生成(无反射,性能高)
// 2. 类型安全(编译期检查依赖)
// 3. 官方支持(Google开源)

//go:generate wire
func InitializeApp() (*App, error) {
    wire.Build(
        NewDB,
        NewRedis,
        NewProductRepo,
        NewProductService,
        NewApp,
    )
    return nil, nil
}

16.4.3 数据库选型

MySQL(主库)

场景选择理由配置
订单表ACID保证、事务支持InnoDB,8分库64表
商品表关联查询、JOIN支持InnoDB,4分库
支付表强一致性、金融级可靠性InnoDB,双主互备

Redis(缓存 + 库存)

场景数据结构TTL
商品详情Hash30分钟
库存数量String(Lua原子扣减)永久
券码池Set(SPOP原子弹出)永久
用户SessionString2小时

Elasticsearch(搜索 + 日志)

场景索引设计刷新间隔
商品搜索product_index(标题、类目、属性)30秒
订单查询order_index(订单号、用户ID、状态)1分钟
日志搜索log-{date}(按日分索引)5秒

16.4.4 中间件选型

Kafka(消息队列)

场景TopicPartitionReplication
订单事件order-events163
库存事件inventory-events83
日志采集logs322

Consul(服务发现)

  • 健康检查:HTTP/TCP/gRPC
  • 配置中心:动态配置热更新
  • KV存储:Feature Flag

Envoy(Service Mesh)

  • 流量管理:灰度发布、A/B测试
  • 可观测性:自动生成Trace
  • 安全:mTLS加密

16.5 核心系统设计

16.5.1 商品中心设计

职责边界

  • ✅ 负责:商品基础信息、类目、属性、多媒体素材
  • ✅ 负责:SPU/SKU管理、上架下架
  • ❌ 不负责:价格(由Pricing Service管理)
  • ❌ 不负责:库存(由Inventory Service管理)

数据模型

// SPU(Standard Product Unit)
type SPU struct {
    ID          int64
    Title       string
    CategoryID  int64
    BrandID     int64
    Attributes  JSONB  // 动态属性(颜色、尺寸、...)
    Images      []string
    Status      string  // DRAFT/ON_SHELF/OFF_SHELF
}

// SKU(Stock Keeping Unit)
type SKU struct {
    ID          int64
    SPUID       int64
    SkuCode     string  // 唯一编码
    Specs       JSONB   // 规格值({"颜色":"红色","尺寸":"L"})
    Status      string
}

分库策略

-- 按 category_id 分库(4分库)
-- 理由:同品类商品通常一起查询
db_index = category_id % 4

-- 商品表不分表
-- 理由:单品类商品数量可控(< 100万)

缓存策略

// L1: 本地缓存(1分钟)
localCache.Set(sku_id, product, 1*time.Minute)

// L2: Redis缓存(30分钟)
redis.Set("product:"+sku_id, marshal(product), 30*time.Minute)

// L3: MySQL(源数据)
db.QueryOne("SELECT * FROM product WHERE sku_id = ?", sku_id)

16.5.2 库存系统设计

二维库存模型(参考16.2.5):

// 库存策略接口
type StockStrategy interface {
    CheckStock(ctx context.Context, req *StockRequest) (*StockResponse, error)
    Reserve(ctx context.Context, req *ReserveRequest) (*ReserveResponse, error)
    Deduct(ctx context.Context, req *DeductRequest) error
    Release(ctx context.Context, reserveID string) error
}

// 策略工厂
func NewStockStrategy(managementType ManagementType) StockStrategy {
    switch managementType {
    case Realtime:
        return &RealtimeStockStrategy{}  // 机票、酒店
    case Pooled:
        return &PooledStockStrategy{}    // 优惠券
    case Unlimited:
        return &UnlimitedStockStrategy{} // 充值
    }
}

预占机制

// Redis Lua脚本(原子预占)
const reserveScript = `
local stock_key = KEYS[1]
local reserve_key = KEYS[2]
local qty = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])

local stock = tonumber(redis.call('GET', stock_key) or 0)
if stock >= qty then
    redis.call('DECRBY', stock_key, qty)
    redis.call('SET', reserve_key, qty, 'EX', ttl)
    return 1
else
    return 0
end
`

func (r *StockRepo) Reserve(ctx context.Context, skuID int64, qty int, ttl time.Duration) (string, error) {
    reserveID := generateReserveID()
    stockKey := fmt.Sprintf("stock:%d", skuID)
    reserveKey := fmt.Sprintf("reserve:%s", reserveID)
    
    result, err := r.redis.Eval(ctx, reserveScript, 
        []string{stockKey, reserveKey}, 
        qty, int(ttl.Seconds())).Result()
    
    if result == int64(1) {
        return reserveID, nil
    }
    return "", errors.New("库存不足")
}

16.5.3 订单系统设计

状态机

type OrderStatus string

const (
    StatusCreated          OrderStatus = "CREATED"           // 已创建
    StatusPendingPayment   OrderStatus = "PENDING_PAYMENT"   // 待支付
    StatusPaid             OrderStatus = "PAID"              // 已支付
    StatusFulfilling       OrderStatus = "FULFILLING"        // 履约中
    StatusFulfilled        OrderStatus = "FULFILLED"         // 已履约
    StatusCanceled         OrderStatus = "CANCELED"          // 已取消
    StatusRefunded         OrderStatus = "REFUNDED"          // 已退款
)

// 状态转换规则
var transitions = map[OrderStatus][]OrderStatus{
    StatusCreated:        {StatusPendingPayment, StatusCanceled},
    StatusPendingPayment: {StatusPaid, StatusCanceled},
    StatusPaid:           {StatusFulfilling, StatusRefunded},
    StatusFulfilling:     {StatusFulfilled, StatusRefunded},
    StatusFulfilled:      {StatusRefunded},  // 已履约可申请退款
}

func (o *Order) TransitionTo(newStatus OrderStatus) error {
    allowed, ok := transitions[o.Status]
    if !ok || !contains(allowed, newStatus) {
        return fmt.Errorf("不允许从 %s 转换到 %s", o.Status, newStatus)
    }
    o.Status = newStatus
    return nil
}

分库分表(参考ADR-007):

• 分库:按 user_id % 8(用户维度查询最频繁)
• 分表:按 create_time 分表(按月归档,64表)
• 路由表:order_route(order_id → db_index, table_index)

16.5.4 支付系统设计

支付流程

// Step 1: 创建支付单
func (s *PaymentService) CreatePayment(ctx context.Context, orderID int64, amount int64) (*Payment, error) {
    payment := &Payment{
        ID:      generatePaymentID(),
        OrderID: orderID,
        Amount:  amount,
        Status:  PaymentStatusCreated,
    }
    s.repo.Save(ctx, payment)
    return payment, nil
}

// Step 2: 调用支付渠道(支付宝/微信)
func (s *PaymentService) Pay(ctx context.Context, paymentID int64, channel string) (*PayURL, error) {
    gateway := s.gatewayFactory.Get(channel)
    payURL, err := gateway.CreateOrder(ctx, payment)
    return payURL, err
}

// Step 3: 接收支付回调(幂等处理)
func (s *PaymentService) HandleCallback(ctx context.Context, callbackData *CallbackData) error {
    // 幂等性检查
    payment, err := s.repo.GetByPaymentID(ctx, callbackData.PaymentID)
    if payment.Status == PaymentStatusPaid {
        return nil  // 已处理,幂等返回
    }
    
    // 验签
    if !s.verifySign(callbackData) {
        return errors.New("签名验证失败")
    }
    
    // 更新支付状态(乐观锁)
    affected, err := s.repo.UpdateStatus(ctx, callbackData.PaymentID, 
        PaymentStatusCreated, PaymentStatusPaid)
    if affected == 0 {
        return errors.New("支付单状态已变更")
    }
    
    // 发布支付成功事件
    s.eventPublisher.Publish(ctx, &PaymentPaidEvent{
        OrderID:   payment.OrderID,
        PaymentID: payment.ID,
        Amount:    payment.Amount,
    })
    
    return nil
}

对账流程

// 每小时对账任务
func (s *PaymentService) ReconcileHourly(ctx context.Context, hour time.Time) error {
    // Step 1: 获取本地支付记录
    localPayments, _ := s.repo.GetByHour(ctx, hour)
    
    // Step 2: 获取支付渠道对账单
    remotePayments, _ := s.gatewayClient.DownloadBill(ctx, hour)
    
    // Step 3: 比对差异
    diff := s.compare(localPayments, remotePayments)
    
    // Step 4: 处理差异
    for _, d := range diff {
        if d.Type == Missing {
            // 本地有,渠道无 → 可能是渠道延迟
            s.alertService.Alert("支付对账差异", d)
        } else if d.Type == Extra {
            // 本地无,渠道有 → 可能是回调丢失
            s.补单处理(d)
        }
    }
    
    return nil
}

16.5.5 供应商集成设计

适配器模式

// 供应商接口(统一抽象)
type SupplierAdapter interface {
    QueryStock(ctx context.Context, req *StockQueryRequest) (*StockQueryResponse, error)
    ReserveStock(ctx context.Context, req *ReserveRequest) (*ReserveResponse, error)
    CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, error)
    QueryOrderStatus(ctx context.Context, orderID string) (*OrderStatus, error)
}

// 机票供应商适配器
type FlightSupplierAdapter struct {
    client *FlightSupplierClient
    config *Config
}

func (a *FlightSupplierAdapter) QueryStock(ctx context.Context, req *StockQueryRequest) (*StockQueryResponse, error) {
    // Step 1: 参数转换(平台模型 → 供应商模型)
    supplierReq := a.transformRequest(req)
    
    // Step 2: 调用供应商API(熔断保护)
    supplierResp, err := a.client.QueryAvailability(ctx, supplierReq)
    if err != nil {
        return nil, fmt.Errorf("供应商调用失败: %w", err)
    }
    
    // Step 3: 响应转换(供应商模型 → 平台模型)
    resp := a.transformResponse(supplierResp)
    return resp, nil
}

熔断机制

import "github.com/sony/gobreaker"

func NewSupplierClientWithCircuitBreaker(client *http.Client) *SupplierClient {
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        "SupplierAPI",
        MaxRequests: 3,
        Interval:    10 * time.Second,
        Timeout:     30 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 3 && failureRatio >= 0.5
        },
        OnStateChange: func(name string, from, to gobreaker.State) {
            log.Printf("熔断器 %s 状态变更: %s -> %s", name, from, to)
        },
    })
    
    return &SupplierClient{
        client: client,
        cb:     cb,
    }
}

func (c *SupplierClient) QueryStock(ctx context.Context, req *Request) (*Response, error) {
    result, err := c.cb.Execute(func() (interface{}, error) {
        return c.client.Do(buildHTTPRequest(req))
    })
    if err != nil {
        return nil, err
    }
    return parseResponse(result), nil
}

16.5.6 商品供给与运营系统

供给运营是电商平台的核心能力,决定了"商品如何进入平台"。本节展示供给侧和运营侧的完整设计。

商品上架系统(从无到有)

业务场景

  • 运营人员手动上传新商品
  • 商家通过Portal批量导入
  • Excel批量上架(节假日大促前)

核心设计

// 上架任务状态机
type ListingStatus string

const (
    ListingDraft      ListingStatus = "DRAFT"       // 草稿
    ListingPending    ListingStatus = "PENDING"     // 待审核
    ListingApproved   ListingStatus = "APPROVED"    // 审核通过
    ListingRejected   ListingStatus = "REJECTED"    // 审核驳回
    ListingPublished  ListingStatus = "PUBLISHED"   // 已发布
)

// 上架任务
type ListingTask struct {
    TaskCode    string        // 幂等性标识
    ItemInfo    ItemInfo      // 商品信息
    SupplierID  int64         // 供应商ID
    Status      ListingStatus
    ReviewerID  int64         // 审核人
    RejectReason string       // 驳回原因
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

// 创建上架任务(幂等性保证)
func (s *ListingService) CreateListingTask(ctx context.Context, req *ListingRequest) (*ListingTask, error) {
    // Step 1: 生成幂等性标识符
    taskCode := s.generateTaskCode(req)
    
    // Step 2: FirstOrCreate(幂等性)
    task := &ListingTask{
        TaskCode:   taskCode,
        ItemInfo:   req.ItemInfo,
        SupplierID: req.SupplierID,
        Status:     ListingDraft,
    }
    
    result := s.db.Where("task_code = ?", taskCode).FirstOrCreate(task)
    if result.RowsAffected > 0 {
        // 首次创建,发布事件
        s.eventPublisher.Publish(ctx, &ListingTaskCreatedEvent{
            TaskCode: taskCode,
            ItemInfo: req.ItemInfo,
        })
    }
    
    return task, nil
}

// 提交审核
func (s *ListingService) SubmitForReview(ctx context.Context, taskCode string) error {
    // 状态转换:DRAFT → PENDING
    return s.updateStatus(ctx, taskCode, ListingDraft, ListingPending)
}

// 审核通过
func (s *ListingService) Approve(ctx context.Context, taskCode string, reviewerID int64) error {
    // Step 1: 状态转换:PENDING → APPROVED
    if err := s.updateStatus(ctx, taskCode, ListingPending, ListingApproved); err != nil {
        return err
    }
    
    // Step 2: 创建商品记录(写入商品中心)
    task, _ := s.getTask(ctx, taskCode)
    itemID, err := s.productCenter.CreateProduct(ctx, &CreateProductRequest{
        ItemInfo:   task.ItemInfo,
        SupplierID: task.SupplierID,
    })
    if err != nil {
        return fmt.Errorf("create product failed: %w", err)
    }
    
    // Step 3: 初始化库存(调用库存服务)
    s.inventoryClient.InitStock(ctx, itemID, task.ItemInfo.InitStock)
    
    // Step 4: 初始化价格(调用计价服务)
    s.pricingClient.InitPrice(ctx, itemID, task.ItemInfo.BasePrice)
    
    // Step 5: 更新搜索索引(异步)
    s.eventPublisher.Publish(ctx, &ProductCreatedEvent{
        ItemID:     itemID,
        ItemInfo:   task.ItemInfo,
        SupplierID: task.SupplierID,
    })
    
    return nil
}

审核策略

审核维度检查项风险等级
合规性违禁词检测、敏感内容
完整性必填字段、图片数量
准确性价格合理性、类目匹配
一致性SPU/SKU关系、属性匹配

供应商同步系统(Upsert场景)

业务场景

  • 供应商定时推送商品数据(每小时/每天)
  • 供应商实时推送价格/库存变更
  • 供应商商品可能已存在,也可能不存在

核心挑战:Upsert语义

如果商品存在 → 更新
如果商品不存在 → 创建

实现方案

// 供应商同步任务
type SyncTask struct {
    SyncID       string    // 同步批次ID
    SupplierID   int64     // 供应商ID
    SupplierSkuID string   // 供应商SKU ID
    SyncData     SyncData  // 同步数据
    SyncType     string    // FULL/INCREMENTAL
    Status       string    // PENDING/SUCCESS/FAILED
}

// Upsert处理(幂等性保证)
func (s *SyncService) UpsertProduct(ctx context.Context, req *SyncRequest) error {
    // Step 1: 根据供应商SKU ID查询平台商品ID
    mapping, err := s.repo.GetMapping(ctx, req.SupplierID, req.SupplierSkuID)
    
    if err == ErrNotFound {
        // 场景1:商品不存在 → 创建(走上架流程)
        return s.createNewProduct(ctx, req)
    } else {
        // 场景2:商品存在 → 更新(走同步流程)
        return s.updateExistingProduct(ctx, mapping.ItemID, req)
    }
}

// 创建新商品(供应商同步触发的上架)
func (s *SyncService) createNewProduct(ctx context.Context, req *SyncRequest) error {
    // Step 1: 创建上架任务
    task, err := s.listingService.CreateListingTask(ctx, &ListingRequest{
        ItemInfo:   transformToItemInfo(req.SyncData),
        SupplierID: req.SupplierID,
        Source:     "SUPPLIER_SYNC",  // 标记来源
    })
    
    // Step 2: 根据供应商信用等级,决定是否需要审核
    if s.needReview(req.SupplierID) {
        // 低信用供应商:需要人工审核
        task.Status = ListingPending
    } else {
        // 高信用供应商:自动通过
        task.Status = ListingApproved
        s.listingService.Approve(ctx, task.TaskCode, SYSTEM_REVIEWER_ID)
    }
    
    return nil
}

// 更新现有商品(供应商同步)
func (s *SyncService) updateExistingProduct(ctx context.Context, itemID int64, req *SyncRequest) error {
    // Step 1: 对比差异
    existing, _ := s.productCenter.GetProduct(ctx, itemID)
    diff := s.compareDiff(existing, req.SyncData)
    
    // Step 2: 根据差异类型决定是否需要审核
    if diff.HasHighRiskChange() {
        // 高风险变更(价格变化>50%、类目变更)→ 需要审核
        return s.createReviewTask(ctx, itemID, diff)
    } else {
        // 低风险变更(库存、图片)→ 直接更新
        return s.productCenter.UpdateProduct(ctx, itemID, diff)
    }
}

// 判断供应商是否需要审核
func (s *SyncService) needReview(supplierID int64) bool {
    supplier, _ := s.supplierRepo.Get(ctx, supplierID)
    
    // 根据供应商信用等级和历史表现决定
    return supplier.CreditLevel < 3 || supplier.RejectRate > 0.1
}

差异化审核策略

变更类型变更范围审核策略理由
价格变更< 10%自动通过正常波动
价格变更10-50%需要审核防止错误
价格变更> 50%必须审核 + 告警高风险
库存变更任意自动通过实时性要求高
标题变更轻微修改自动通过低风险
类目变更任意必须审核影响搜索
图片变更任意自动通过低风险

运营编辑系统(日常维护)

业务场景

  • 单品编辑(修改标题、描述、图片)
  • 批量编辑(批量调价、批量上下架)
  • 批量导入导出(Excel操作)

核心设计

// 运营编辑任务
type EditTask struct {
    TaskID      string       // 任务ID
    ItemIDs     []int64      // 商品ID列表(支持批量)
    EditType    string       // SINGLE/BATCH
    Changes     []Change     // 变更内容
    Status      string       // PENDING/EXECUTING/SUCCESS/FAILED
    Progress    int          // 进度(0-100)
    TotalCount  int          // 总数
    SuccessCount int         // 成功数
    FailedCount int          // 失败数
}

// 批量编辑(异步任务)
func (s *EditService) BatchEdit(ctx context.Context, req *BatchEditRequest) (*EditTask, error) {
    // Step 1: 创建批量编辑任务
    task := &EditTask{
        TaskID:     generateTaskID(),
        ItemIDs:    req.ItemIDs,
        EditType:   "BATCH",
        Changes:    req.Changes,
        Status:     "PENDING",
        TotalCount: len(req.ItemIDs),
    }
    s.taskRepo.Save(ctx, task)
    
    // Step 2: 发布异步任务
    s.taskQueue.Publish(ctx, &BatchEditTaskEvent{
        TaskID: task.TaskID,
    })
    
    return task, nil
}

// 批量编辑执行器(异步)
func (w *BatchEditWorker) Execute(ctx context.Context, taskID string) error {
    task, _ := w.taskRepo.Get(ctx, taskID)
    
    // 逐个处理商品
    for i, itemID := range task.ItemIDs {
        err := w.editSingleItem(ctx, itemID, task.Changes)
        
        if err == nil {
            task.SuccessCount++
        } else {
            task.FailedCount++
            log.Errorf("edit item %d failed: %v", itemID, err)
        }
        
        // 更新进度
        task.Progress = (i + 1) * 100 / task.TotalCount
        w.taskRepo.Update(ctx, task)
    }
    
    // 更新任务状态
    if task.FailedCount == 0 {
        task.Status = "SUCCESS"
    } else if task.SuccessCount == 0 {
        task.Status = "FAILED"
    } else {
        task.Status = "PARTIAL_SUCCESS"
    }
    
    return nil
}

进度追踪

// 查询任务进度
func (s *EditService) GetTaskProgress(ctx context.Context, taskID string) (*TaskProgress, error) {
    task, _ := s.taskRepo.Get(ctx, taskID)
    
    return &TaskProgress{
        TaskID:       task.TaskID,
        Status:       task.Status,
        Progress:     task.Progress,
        TotalCount:   task.TotalCount,
        SuccessCount: task.SuccessCount,
        FailedCount:  task.FailedCount,
        EstimateLeft: s.estimateTimeLeft(task),
    }, nil
}

16.5.7 C端交易流完整链路

交易流是电商的核心价值链,从用户搜索到完成支付的完整路径。本节展示五个阶段的设计与集成。

阶段1:搜索与导购

业务场景:用户搜索"iPhone 15"

系统架构

用户输入关键词
    ↓
API Gateway → Search Aggregation
    ↓
Query理解(分词、纠错、意图识别)
    ↓
Elasticsearch召回(相关性排序)
    ↓
Hydrate编排(并发调用多个服务)
    ├─ Product Service(商品信息)
    ├─ Inventory Service(库存状态)
    ├─ Pricing Service(价格计算)
    └─ Marketing Service(活动标签)
    ↓
返回搜索结果

核心代码

// 搜索聚合服务
type SearchAggregation struct {
    esClient        *elasticsearch.Client
    productClient   rpc.ProductClient
    inventoryClient rpc.InventoryClient
    pricingClient   rpc.PricingClient
    marketingClient rpc.MarketingClient
}

func (a *SearchAggregation) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
    // Step 1: Query理解(分词、意图识别)
    query := a.parseQuery(req.Keyword)
    
    // Step 2: ES召回(按相关性排序)
    hits, err := a.esClient.Search(ctx, query)
    if err != nil {
        return nil, err
    }
    
    skuIDs := extractSkuIDs(hits)
    
    // Step 3: Hydrate编排(并发调用)
    var products map[int64]*Product
    var stocks map[int64]*Stock
    var prices map[int64]*Price
    var promos map[int64]*PromoInfo
    
    g, ctx := errgroup.WithContext(ctx)
    
    // 并发调用4个服务
    g.Go(func() error {
        products, _ = a.productClient.BatchGet(ctx, skuIDs)
        return nil
    })
    g.Go(func() error {
        stocks, _ = a.inventoryClient.BatchCheck(ctx, skuIDs)
        return nil
    })
    g.Go(func() error {
        priceItems := buildPriceItems(skuIDs)
        prices, _ = a.pricingClient.BatchCalculate(ctx, priceItems)
        return nil
    })
    g.Go(func() error {
        promos, _ = a.marketingClient.BatchGet(ctx, skuIDs, req.UserID)
        // 降级:Marketing故障时使用空促销
        if promos == nil {
            promos = make(map[int64]*PromoInfo)
        }
        return nil
    })
    
    g.Wait()
    
    // Step 4: 聚合结果
    return a.buildSearchResponse(hits, products, stocks, prices, promos), nil
}

性能优化

  • ES查询:P99 < 50ms
  • Hydrate并发:4个服务并发调用,总耗时 < 200ms
  • 缓存策略:热门搜索词缓存5分钟

阶段2:商品详情页(PDP)

业务场景:用户点击商品进入详情页

核心设计

// 详情页聚合服务
func (a *DetailAggregation) GetDetail(ctx context.Context, skuID int64, userID int64) (*DetailResponse, error) {
    // 并发调用5个服务
    var product *Product
    var stock *Stock
    var price *Price
    var promos []*Promotion
    var reviews []*Review
    
    g, ctx := errgroup.WithContext(ctx)
    
    g.Go(func() error {
        product, _ = a.productClient.Get(ctx, skuID)
        return nil
    })
    g.Go(func() error {
        stock, _ = a.inventoryClient.Check(ctx, skuID)
        return nil
    })
    g.Go(func() error {
        price, _ = a.pricingClient.Calculate(ctx, skuID, userID)
        return nil
    })
    g.Go(func() error {
        promos, _ = a.marketingClient.GetPromotions(ctx, skuID, userID)
        return nil
    })
    g.Go(func() error {
        reviews, _ = a.reviewClient.GetTopReviews(ctx, skuID, 5)
        return nil
    })
    
    g.Wait()
    
    // 生成快照(用于后续试算)
    snapshot := a.generateSnapshot(product, price, promos)
    
    return &DetailResponse{
        Product:   product,
        Stock:     stock,
        Price:     price,
        Promos:    promos,
        Reviews:   reviews,
        Snapshot:  snapshot,  // 快照ID,5分钟有效
    }, nil
}

阶段3:购物车

业务场景:用户加购商品

未登录加购

// 未登录用户(Cookie存储)
func (c *CartService) AddToCartAnonymous(ctx context.Context, req *AddCartRequest) error {
    // Step 1: 获取匿名cartID(存储在Cookie)
    cartID := req.AnonymousCartID
    if cartID == "" {
        cartID = generateCartID()
    }
    
    // Step 2: 存储到Redis(TTL=7天)
    cartKey := fmt.Sprintf("cart:anon:%s", cartID)
    cartData, _ := c.redis.Get(ctx, cartKey).Result()
    
    cart := parseCart(cartData)
    cart.AddItem(req.SkuID, req.Quantity)
    
    c.redis.Set(ctx, cartKey, marshal(cart), 7*24*time.Hour)
    
    return nil
}

登录后合并

// 用户登录后合并购物车
func (c *CartService) MergeCartOnLogin(ctx context.Context, userID int64, anonymousCartID string) error {
    // Step 1: 获取匿名购物车
    anonCartKey := fmt.Sprintf("cart:anon:%s", anonymousCartID)
    anonCart, _ := c.redis.Get(ctx, anonCartKey).Result()
    
    // Step 2: 获取用户购物车
    userCartKey := fmt.Sprintf("cart:user:%d", userID)
    userCart, _ := c.redis.Get(ctx, userCartKey).Result()
    
    // Step 3: 合并(相同商品累加数量)
    merged := mergeCarts(parseCart(anonCart), parseCart(userCart))
    
    // Step 4: 保存到用户购物车
    c.redis.Set(ctx, userCartKey, marshal(merged), 0)  // 永久存储
    
    // Step 5: 删除匿名购物车
    c.redis.Del(ctx, anonCartKey)
    
    // Step 6: 异步持久化到MySQL(防止Redis丢失)
    c.eventPublisher.Publish(ctx, &CartMergedEvent{
        UserID: userID,
        Items:  merged.Items,
    })
    
    return nil
}

阶段4:结算页试算

业务场景:用户点击"去结算"

核心设计

// 结算页聚合服务
func (a *CheckoutAggregation) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
    // Step 1: 判断是否使用快照(ADR-008)
    var products []*Product
    var promos []*Promotion
    
    if req.Snapshot != nil && !req.Snapshot.IsExpired() {
        // 快照未过期,使用快照数据(性能优先)
        products = req.Snapshot.Products
        promos = req.Snapshot.Promos
    } else {
        // 快照过期,实时查询
        products, _ = a.productClient.BatchGet(ctx, req.SkuIDs)
        promos, _ = a.marketingClient.GetPromotions(ctx, req.SkuIDs, req.UserID)
    }
    
    // Step 2: 实时查询库存(不能用快照)
    stocks, _ := a.inventoryClient.BatchCheck(ctx, req.SkuIDs)
    
    // Step 3: 计算价格
    prices, _ := a.pricingClient.BatchCalculate(ctx, products, promos)
    
    // Step 4: 检查可下单性
    canCheckout := a.checkCanCheckout(stocks, req.Items)
    
    return &CalculateResponse{
        Items:       buildItems(products, stocks, prices),
        TotalPrice:  calculateTotal(prices),
        CanCheckout: canCheckout,
        Warnings:    a.generateWarnings(stocks, promos),
    }, nil
}

阶段5:下单与支付

完整下单流程(Saga模式):

// 订单创建Saga(编排多个服务调用)
type CreateOrderSaga struct {
    productClient   rpc.ProductClient
    inventoryClient rpc.InventoryClient
    pricingClient   rpc.PricingClient
    marketingClient rpc.MarketingClient
    orderRepo       *OrderRepo
}

func (s *CreateOrderSaga) Execute(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    var err error
    var reserved *ReserveResult
    var couponLock *CouponLock
    
    // Step 1: 实时查询商品信息(ADR-009:不使用快照)
    products, err := s.productClient.BatchGet(ctx, req.SkuIDs)
    if err != nil {
        return nil, fmt.Errorf("query products failed: %w", err)
    }
    
    // Step 2: 实时查询营销信息
    promos, err := s.marketingClient.GetPromotions(ctx, req.SkuIDs, req.UserID)
    if err != nil {
        return nil, fmt.Errorf("query promotions failed: %w", err)
    }
    
    // Step 3: 校验营销活动有效性
    for _, promo := range promos {
        if !s.validatePromotion(promo) {
            return nil, fmt.Errorf("promotion %s expired", promo.ID)
        }
    }
    
    // Step 4: 库存预占(CAS操作)
    reserved, err = s.inventoryClient.Reserve(ctx, req.Items)
    if err != nil {
        return nil, fmt.Errorf("库存不足: %w", err)
    }
    defer func() {
        if err != nil {
            // 补偿:释放库存
            s.inventoryClient.Release(ctx, reserved.ReserveID)
        }
    }()
    
    // Step 5: 优惠券锁定
    if req.CouponCode != "" {
        couponLock, err = s.marketingClient.LockCoupon(ctx, req.CouponCode, req.UserID)
        if err != nil {
            return nil, fmt.Errorf("优惠券锁定失败: %w", err)
        }
        defer func() {
            if err != nil {
                // 补偿:释放优惠券
                s.marketingClient.UnlockCoupon(ctx, couponLock.LockID)
            }
        }()
    }
    
    // Step 6: 实时计算价格
    price, err := s.pricingClient.Calculate(ctx, products, promos)
    if err != nil {
        return nil, fmt.Errorf("价格计算失败: %w", err)
    }
    
    // Step 7: 价格校验(ADR-011)
    if req.ExpectedPrice > 0 {
        if err := s.validatePriceChange(req.ExpectedPrice, price.FinalPrice); err != nil {
            return nil, err
        }
    }
    
    // Step 8: 生成商品快照
    snapshot := s.generateProductSnapshot(products, promos, price)
    
    // Step 9: 创建订单
    order := &Order{
        OrderID:         s.generateOrderID(),
        UserID:          req.UserID,
        Items:           req.Items,
        TotalPrice:      price.FinalPrice,
        ProductSnapshot: marshal(snapshot),
        ReserveID:       reserved.ReserveID,
        CouponLockID:    couponLock.LockID,
        Status:          StatusPendingPayment,
        ExpireTime:      time.Now().Add(15 * time.Minute),
    }
    
    err = s.orderRepo.Create(ctx, order)
    if err != nil {
        return nil, fmt.Errorf("订单创建失败: %w", err)
    }
    
    // Step 10: 发布订单创建事件(异步)
    s.eventPublisher.Publish(ctx, &OrderCreatedEvent{
        OrderID: order.OrderID,
        UserID:  order.UserID,
        Items:   order.Items,
    })
    
    return order, nil
}

交易流监控

阶段关键指标目标值
搜索搜索→点击转化率> 15%
详情页详情→加购转化率> 8%
购物车加购→结算转化率> 30%
结算页结算→下单转化率> 60%
支付下单→支付转化率> 85%
整体搜索→支付转化率> 2%

16.5.8 DDD战术设计实践

领域模型是系统设计的核心。本节展示如何在订单域应用DDD战术模式。

聚合设计:Order聚合根

// Order聚合根
type Order struct {
    // 聚合根ID
    orderID OrderID  // 值对象
    
    // 基本信息
    userID    int64
    shopID    int64
    
    // 订单明细(实体集合)
    items []*OrderItem
    
    // 价格信息(值对象)
    pricing *OrderPricing
    
    // 状态(值对象)
    status OrderStatus
    
    // 时间戳
    createdAt time.Time
    updatedAt time.Time
    
    // 领域事件(未提交)
    domainEvents []DomainEvent
}

// 值对象:OrderID
type OrderID struct {
    value string
}

func NewOrderID() OrderID {
    return OrderID{value: generateSnowflakeID()}
}

func (id OrderID) String() string {
    return id.value
}

// 值对象:OrderPricing
type OrderPricing struct {
    subtotal       int64  // 商品总价
    discount       int64  // 折扣金额
    couponDiscount int64  // 优惠券
    payableAmount  int64  // 应付金额
}

func (p *OrderPricing) Calculate() int64 {
    return p.subtotal - p.discount - p.couponDiscount
}

// 实体:OrderItem
type OrderItem struct {
    itemID    int64
    skuID     int64
    quantity  int
    unitPrice int64
    
    // 快照
    snapshot *ItemSnapshot
}

// 聚合根方法:状态转换
func (o *Order) TransitionTo(newStatus OrderStatus) error {
    // 检查状态转换是否合法
    if !o.status.CanTransitionTo(newStatus) {
        return fmt.Errorf("不允许从 %s 转换到 %s", o.status, newStatus)
    }
    
    oldStatus := o.status
    o.status = newStatus
    o.updatedAt = time.Now()
    
    // 发布领域事件
    o.addDomainEvent(&OrderStatusChangedEvent{
        OrderID:   o.orderID,
        OldStatus: oldStatus,
        NewStatus: newStatus,
        ChangedAt: o.updatedAt,
    })
    
    return nil
}

// 聚合根方法:添加商品项
func (o *Order) AddItem(item *OrderItem) error {
    // 不变量检查:订单金额不能超过限额
    if o.calculateTotal()+item.Total() > MAX_ORDER_AMOUNT {
        return errors.New("订单金额超过限额")
    }
    
    o.items = append(o.items, item)
    
    // 发布领域事件
    o.addDomainEvent(&OrderItemAddedEvent{
        OrderID: o.orderID,
        Item:    item,
    })
    
    return nil
}

// 不变量:订单金额 = 所有商品项之和
func (o *Order) calculateTotal() int64 {
    total := int64(0)
    for _, item := range o.items {
        total += item.Total()
    }
    return total
}

// 领域事件管理
func (o *Order) addDomainEvent(event DomainEvent) {
    o.domainEvents = append(o.domainEvents, event)
}

func (o *Order) DomainEvents() []DomainEvent {
    return o.domainEvents
}

func (o *Order) ClearDomainEvents() {
    o.domainEvents = nil
}

Repository模式

// OrderRepository接口(领域层定义)
type OrderRepository interface {
    Save(ctx context.Context, order *Order) error
    FindByID(ctx context.Context, orderID OrderID) (*Order, error)
    FindByUserID(ctx context.Context, userID int64, limit int) ([]*Order, error)
}

// OrderRepositoryImpl实现(基础设施层)
type OrderRepositoryImpl struct {
    db            *gorm.DB
    eventPublisher EventPublisher
}

func (r *OrderRepositoryImpl) Save(ctx context.Context, order *Order) error {
    // Step 1: 转换聚合根 → 数据模型
    orderDO := r.toDataObject(order)
    
    // Step 2: 保存到数据库
    err := r.db.Transaction(func(tx *gorm.DB) error {
        // 保存订单主表
        if err := tx.Create(orderDO).Error; err != nil {
            return err
        }
        
        // 保存订单明细表
        for _, item := range order.Items() {
            itemDO := r.toItemDataObject(item, orderDO.ID)
            if err := tx.Create(itemDO).Error; err != nil {
                return err
            }
        }
        
        return nil
    })
    
    if err != nil {
        return err
    }
    
    // Step 3: 发布领域事件(事务提交后)
    for _, event := range order.DomainEvents() {
        r.eventPublisher.Publish(ctx, event)
    }
    order.ClearDomainEvents()
    
    return nil
}

领域事件与Outbox模式

// Outbox表(确保事件必达)
type Outbox struct {
    ID          int64
    EventType   string
    EventData   string  // JSON
    Status      string  // PENDING/PUBLISHED/FAILED
    RetryCount  int
    CreatedAt   time.Time
}

// 发布领域事件(Outbox模式)
func (p *EventPublisher) Publish(ctx context.Context, event DomainEvent) error {
    // Step 1: 序列化事件
    eventData, _ := json.Marshal(event)
    
    // Step 2: 写入Outbox表(与业务在同一事务)
    outbox := &Outbox{
        EventType: event.Type(),
        EventData: string(eventData),
        Status:    "PENDING",
        CreatedAt: time.Now(),
    }
    
    return p.db.Create(outbox).Error
}

// Outbox轮询器(定时扫描未发布的事件)
func (w *OutboxWorker) Run() {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        // Step 1: 查询待发布事件(PENDING状态)
        var outboxes []*Outbox
        w.db.Where("status = ? AND retry_count < ?", "PENDING", 3).
            Limit(100).
            Find(&outboxes)
        
        // Step 2: 发布到Kafka
        for _, outbox := range outboxes {
            err := w.kafkaProducer.Send(outbox.EventType, outbox.EventData)
            
            if err == nil {
                // 发布成功,标记为PUBLISHED
                w.db.Model(outbox).Update("status", "PUBLISHED")
            } else {
                // 发布失败,重试计数+1
                w.db.Model(outbox).Updates(map[string]interface{}{
                    "retry_count": gorm.Expr("retry_count + 1"),
                    "status":      "FAILED",
                })
            }
        }
    }
}

16.5.6 架构决策记录(ADR)

本节记录系统设计过程中的关键架构决策,包括决策背景、备选方案、最终决策及理由。ADR是架构演进的重要资产,帮助团队理解「为什么这样设计」,避免重复讨论。

ADR-001: 计价中心数据输入方式

决策日期:2026-04-14
状态:已采纳 ✓

问题描述:计价中心需要营销信息(促销规则、优惠券等)来计算最终价格,有两种方案:

  • 方案1:计价中心自己调用Marketing Service获取营销信息
  • 方案2:聚合服务获取营销信息后传递给计价中心

决策:采用方案2,由聚合服务获取营销信息后传递给计价中心。

理由

  1. 单一职责原则(SRP)

    • Pricing Service专注于价格计算逻辑(纯函数)
    • Aggregation Service负责数据编排和获取
    • 职责边界清晰,符合微服务设计原则
  2. 依赖解耦

    方案1依赖链:Aggregation → Pricing → Marketing(传递性依赖)
    方案2依赖链:Aggregation → Pricing | Marketing(平行依赖)✓
    
  3. 性能优化空间更大

    • 聚合层可以并发调用Marketing和其他服务(Product、Inventory)
    • Pricing变成纯计算,无IO等待
    • 减少网络调用层级(2层 vs 3层)
  4. 易于测试

    // 方案2:Pricing是纯函数,测试简单
    func TestCalculatePrice(t *testing.T) {
        priceItem := &PriceCalculateItem{
            SkuID:     1001,
            BasePrice: 2399.00,
            PromoInfo: &PromoInfo{DiscountRate: 0.9},  // Mock数据
        }
        result := pricingService.Calculate(priceItem)
        assert.Equal(t, 2159.10, result.FinalPrice)
    }
    
  5. 统一降级处理

    • 聚合层统一处理各服务失败(Marketing、Product、Inventory)
    • Pricing Service无感知,始终收到完整输入数据
    • 降级逻辑不混入业务计算

代码示例

// SearchOrchestrator(聚合服务)
func (o *SearchOrchestrator) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
    // Step 1: 获取sku_ids(从ES)
    skuIDs, _ := o.searchClient.QuerySkuIDs(ctx, req.Keyword)
    
    // Step 2: 并发调用Product + Inventory + Marketing
    var products []*Product
    var stocks []*Stock
    var promos map[int64]*PromoInfo
    
    g, ctx := errgroup.WithContext(ctx)
    g.Go(func() error {
        products, _ = o.productClient.BatchGet(ctx, skuIDs)
        return nil
    })
    g.Go(func() error {
        stocks, _ = o.inventoryClient.BatchCheck(ctx, skuIDs)
        return nil
    })
    g.Go(func() error {
        promos, _ = o.marketingClient.BatchGet(ctx, skuIDs, req.UserID)
        // 降级:Marketing故障时使用空促销
        if promos == nil {
            promos = make(map[int64]*PromoInfo)
        }
        return nil
    })
    g.Wait()
    
    // Step 3: 调用Pricing计算价格(传入营销信息)
    priceItems := buildPriceItems(products, promos)
    prices, _ := o.pricingClient.BatchCalculate(ctx, priceItems)
    
    return buildSearchResponse(products, stocks, prices), nil
}

// PricingService(计价中心)- 纯函数,只负责计算
func (s *PricingService) Calculate(item *PriceItem) *PriceResult {
    finalPrice := item.BasePrice
    
    // 应用促销折扣(数据来自聚合层)
    if item.PromoInfo != nil {
        finalPrice = finalPrice * item.PromoInfo.DiscountRate
    }
    
    return &PriceResult{
        OriginalPrice: item.BasePrice,
        FinalPrice:    finalPrice,
        Discount:      item.BasePrice - finalPrice,
    }
}

影响范围

  • Aggregation Service:增加Marketing Service调用
  • Pricing Service:接收PromoInfo作为输入参数
  • Marketing Service:无影响

ADR-002: 库存预占时机

决策日期:2026-04-14
状态:已采纳 ✓

问题描述:在下单流程中,库存预占的时机有两种选择:

  • 方案1:结算试算时预占(早期锁定)
  • 方案2:确认下单时预占(延迟锁定)

决策:采用方案2,在确认下单时预占库存。

理由

  1. 减少无效预占

    • 用户在试算阶段可能多次修改商品、数量、优惠券
    • 早期预占会导致大量无效锁定(用户未真正下单)
    • 试算到下单的转化率通常只有20-30%
  2. 提升库存利用率

    • 避免库存被长时间预占(用户可能犹豫、放弃)
    • 预占时长控制在15分钟内(支付超时自动释放)
  3. 降低系统压力

    • 试算接口QPS高(用户多次试算),预占会导致Redis压力大
    • 确认下单QPS相对较低,预占操作更可控
  4. 用户体验

    • 试算快速返回(不需要等待预占操作)
    • 确认下单时再预占,用户心理准备更充分

权衡

  • ✓ 优点:提升库存利用率、减少无效预占、降低系统压力
  • ✗ 缺点:确认下单时可能库存不足(需要前端提示)

降低缺点的措施

  • 试算时展示实时库存状态("仅剩N件")
  • 确认下单时二次校验库存,失败友好提示
  • 热门商品提前告知"库存紧张,请尽快下单"

ADR-003: 聚合服务 vs BFF

决策日期:2026-04-14
状态:已采纳 ✓

问题描述:在API Gateway和微服务之间,是使用BFF(Backend For Frontend)还是Aggregation Service?

决策:采用Aggregation Service,而不是传统BFF。

理由

  1. 业务导向 vs 端导向

    • BFF按端划分(Web BFF、App BFF、小程序 BFF)
    • Aggregation按业务场景划分(搜索聚合、详情聚合、结算聚合)✓
    • 本系统多个端(Web、App)的业务逻辑高度一致,按端拆分会导致重复代码
  2. 代码复用

    BFF模式:
    ├─ Web BFF(搜索逻辑)
    ├─ App BFF(搜索逻辑)    ← 重复代码
    └─ 小程序 BFF(搜索逻辑) ← 重复代码
    
    Aggregation模式:✓
    ├─ Search Aggregation(Web/App/小程序共用)
    └─ Detail Aggregation(Web/App/小程序共用)
    
  3. 维护成本

    • BFF需要维护多个端的代码一致性
    • Aggregation只需维护一套业务逻辑
  4. 适配端差异的方式

    • API Gateway层处理端协议差异(HTTP、WebSocket、gRPC)
    • Aggregation返回标准数据格式,前端各端按需裁剪

适用场景

  • ✓ 多端业务逻辑高度一致(如本系统)
  • ✗ 不适用:各端业务逻辑差异大(如社交产品,Feed流算法不同)

ADR-004: 虚拟商品库存模型

决策日期:2026-04-14
状态:已采纳 ✓

问题描述:虚拟商品(机票、充值卡、优惠券)的库存模型和实物商品差异大,应该如何设计?

决策:采用二维库存模型(ManagementType + UnitType)。

库存管理类型(ManagementType)

类型说明典型品类库存来源
实时库存强依赖供应商实时查询机票、酒店供应商API
池化库存自有库存,可超卖后补偿充值卡、优惠券平台采购
无限库存虚拟商品,无库存限制SaaS服务、数字内容

库存单位类型(UnitType)

类型说明典型品类
SKU级别每个规格独立库存充电器(颜色、规格)
批次级别按批次管理(有效期)优惠券、礼品卡
座位级别唯一标识(座位号)机票、电影票

理由

  1. 不同品类的库存特性差异极大,无法用统一模型
  2. 二维模型提供灵活性,支持策略模式动态选择
  3. 便于扩展新品类(只需添加新策略)

ADR-005: 同步 vs 异步数据流

决策日期:2026-04-14
状态:已采纳 ✓

问题描述:下单流程中,哪些操作应该同步执行,哪些应该异步执行?

决策:采用同步+异步混合模式

同步操作(用户等待)

  1. 库存预占(必须成功,否则无法下单)
  2. 优惠券扣减(避免超发)
  3. 订单创建(生成order_id)

异步操作(Kafka事件)

  1. 库存确认扣减(预占成功后,异步确认)
  2. 搜索索引更新(销量、热度)
  3. 购物车清理
  4. 用户行为分析
  5. 消息通知(订单确认、物流更新)

理由

  1. 用户体验

    • 同步操作<500ms,用户可接受
    • 非核心操作异步化,不阻塞下单
  2. 系统解耦

    • 异步事件降低服务间强依赖
    • 消费者故障不影响下单流程
  3. 性能优化

    • 减少下单接口响应时间
    • 异步操作可批量处理(提升吞吐)
  4. 容错能力

    • 异步操作支持重试(Kafka消费者重试机制)
    • 同步操作失败可立即回滚(Saga模式)

ADR-009: 创单时是否使用快照数据(核心安全决策)

决策日期:2026-04-15
状态:已采纳 ✓

问题描述:用户从详情页到提交订单期间,前端已经缓存了商品信息、价格、活动等快照数据。在用户点击"提交订单"创建订单时,后端是否可以使用这些快照数据来提升性能,避免重复查询?

备选方案

方案描述优点缺点
方案A:使用快照创单时直接使用前端传递的快照数据✅ 性能好(无需查询)
✅ 响应快(200ms → 50ms)
❌ 安全风险高(快照可能被篡改)
❌ 资损风险
方案B:强制实时查询创单时强制调用商品服务、营销服务查询最新数据✅ 数据绝对准确
✅ 安全性高(防篡改)
✅ 无资损风险
❌ 性能稍差(多次RPC调用)
❌ RT增加100-200ms
方案C:混合模式普通商品用快照,营销商品强制查询⚠️ 复杂度高
⚠️ 容易出错
❌ 维护成本高
❌ 边界不清晰

决策:采用方案B(强制实时查询)

决策理由

  1. 安全性优先于性能

    风险分析:
    - 如果用快照,活动结束但快照未更新 → 用户用秒杀价下单 → 资损
    - 如果用快照,用户篡改价格 → 恶意低价下单 → 资损
    - 性能损失:100-200ms
    - 资损风险:每单可能损失数百至数千元
    
    结论:100ms的性能代价 << 资损风险
    
  2. 涉及资金的操作必须实时校验

    创单 = 锁定库存 + 锁定价格 + 准备扣款
    → 必须基于最新、最准确的数据
    → 不能因为性能优化而妥协安全性
    
  3. 防止恶意篡改

    场景:黑产抓包修改快照数据
    快照:{"expected_payable": 799900}  // 原价 ¥7,999
    篡改:{"expected_payable": 1}       // 改成 ¥0.01
    
    如果后端使用快照:
    → 按 ¥0.01 创单 → 公司巨额损失!
    
    强制实时查询:
    → 后端查到实际价格 ¥7,999
    → 对比快照 ¥0.01 vs 实际 ¥7,999
    → 差异巨大,拒绝创单!
    
  4. 活动可能随时变化

    10:00  秒杀价 ¥7,999,生成快照
    10:04  秒杀活动提前结束(库存售罄)
    10:05  用户提交订单
    
    如果用快照:
    → 按 ¥7,999 创单(活动已结束!)
    → 资损
    
    强制查询:
    → 查到活动已结束,价格 ¥8,999
    → 提示用户价格变化
    → 避免资损
    

实现方案

// OrderService.CreateOrder - 确认下单接口(准确性优先)
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    // ⚠️ 关键:创单时不使用任何前端传递的快照数据,全部实时查询
    
    // Step 1: 实时查询商品信息(不使用前端快照)
    products, err := s.productClient.BatchGetProducts(ctx, req.SkuIDs)
    if err != nil {
        return nil, fmt.Errorf("query products failed: %w", err)
    }
    
    // Step 2: 实时查询营销活动(强制最新数据)
    promos, err := s.marketingClient.BatchGetPromotions(ctx, req.SkuIDs, req.UserID)
    if err != nil {
        return nil, fmt.Errorf("query promotions failed: %w", err)
    }
    
    // Step 3: 校验营销活动有效性(关键:防止使用过期活动)
    for _, promo := range promos {
        if !s.validatePromotion(promo) {
            return nil, fmt.Errorf("promotion %s is invalid or expired", promo.ID)
        }
    }
    
    // Step 4: 实时计算价格(基于最新营销数据)
    price, err := s.pricingClient.CalculateFinalPrice(ctx, products, promos)
    if err != nil {
        return nil, fmt.Errorf("calculate price failed: %w", err)
    }
    
    // Step 5: 价格校验(对比前端传递的期望价格)
    if req.ExpectedPrice > 0 {
        if err := s.validatePriceChange(req.ExpectedPrice, price.FinalPrice); err != nil {
            return nil, err  // 价格变化过大,拒绝创单
        }
    }
    
    // Step 6: 预占库存
    reserved, err := s.inventoryClient.ReserveStock(ctx, req.Items)
    if err != nil {
        return nil, fmt.Errorf("reserve stock failed: %w", err)
    }
    
    // Step 7: 生成商品快照(基于实时查询的数据)
    snapshot := s.generateProductSnapshot(products, promos, price)
    
    // Step 8: 创建订单(保存快照)
    order := &Order{
        OrderID:         s.generateOrderID(),
        UserID:          req.UserID,
        Items:           req.Items,
        TotalPrice:      price.FinalPrice,
        ProductSnapshot: marshal(snapshot),  // 💾 保存商品快照
        Status:          OrderStatusPendingPayment,
        ExpireTime:      time.Now().Add(15 * time.Minute),
        ReserveIDs:      reserved,
    }
    
    return s.orderRepo.Create(ctx, order)
}

// 价格校验逻辑(防止用户感知差)
func (s *OrderService) validatePriceChange(expected, actual int64) error {
    diff := actual - expected
    diffPercent := float64(diff) / float64(expected) * 100
    
    // 场景1: 价格降低 → 允许(对用户有利)
    if diff < 0 {
        return nil
    }
    
    // 场景2: 价格上涨 < 1元 → 允许(误差容忍)
    if diff <= 100 { // 100分 = 1元
        return nil
    }
    
    // 场景3: 价格上涨 >= 1元 且 < 5% → 允许但记录日志
    if diffPercent < 5.0 {
        log.Warnf("price increased: expected=%d, actual=%d", expected, actual)
        return nil
    }
    
    // 场景4: 价格上涨 >= 5% → 拒绝,要求用户重新确认
    return &PriceChangedError{
        Expected: expected,
        Actual:   actual,
        Message:  fmt.Sprintf("价格已变化,请重新确认"),
    }
}

核心原则

┌────────────────────────────────────────────────────────┐
│ 试算阶段:性能优先 → 可用快照(5分钟缓存)              │
│ 创单阶段:准确性优先 → 强制实时查询                     │
│ 历史查询:可追溯性 → 保存快照到订单表                   │
└────────────────────────────────────────────────────────┘

ADR-010: 创单与支付的时序关系

决策日期:2026-04-14
状态:已采纳 ✓

问题描述:在订单流程中,"创建订单"和"支付"这两个动作的时序关系有两种模式:

  1. 创单即支付:用户点击"立即购买"后,先支付,支付成功后再创建订单
  2. 先创单后支付:用户点击"提交订单"后,先创建订单(资源扣减),然后再支付

决策:采用"先创单后支付"模式

理由

1. 防止超卖(关键)

【创单即支付模式的问题】:
1. 用户A看到库存=1
2. 用户B也看到库存=1
3. 用户A点击支付(此时库存未扣减)
4. 用户B也点击支付(库存仍未扣减)
5. 两人同时支付成功 → 超卖!

【先创单后支付模式的解决方案】:
1. 用户A点击"提交订单" → 库存预占:1 → 0(剩余可用)
2. 用户B点击"提交订单" → 库存不足,下单失败
3. 用户A有15分钟支付窗口
4. 如果用户A超时未支付 → 释放库存:0 → 1(其他人可下单)

2. 用户体验更好

  • ✅ 用户点击"提交订单"后,订单立即生成,库存被锁定
  • ✅ 用户可以慢慢选择支付方式(支付宝、微信、银行卡)
  • ✅ 用户可以在支付环节选择优惠券、支付渠道优惠
  • ✅ 用户可以先下单占位,稍后再支付(适合机票、酒店)

3. 价格计算灵活性

  • 创单时计算:商品基础价格 + 营销优惠(折扣、满减)
  • 支付时计算:支付渠道费(信用卡手续费、花呗分期费)+ 支付渠道优惠

权衡

维度优势劣势
用户体验✅ 先锁定库存,再支付
✅ 支付环节更灵活
⚠️ 15分钟内库存被占用
防止超卖✅ 创单时锁定库存(零超卖)⚠️ 需要处理超时释放逻辑
库存利用率⚠️ 预占库存可能被浪费(10-20%未支付率)✅ 可通过缩短支付窗口优化
系统复杂度⚠️ 需要库存预占机制
⚠️ 需要超时释放定时任务
⚠️ 状态机更复杂

超时未支付处理

// OrderTimeoutJob - 定时扫描超时未支付订单
func (j *OrderTimeoutJob) Run() {
    // 查询超时订单(创建时间 > 15分钟,状态=PENDING_PAYMENT)
    expiredOrders := j.orderRepo.FindExpiredPendingPayment(15 * time.Minute)
    
    for _, order := range expiredOrders {
        // 1. 更新订单状态:PENDING_PAYMENT → CANCELLED
        order.Status = OrderStatusCancelled
        order.CancelReason = "超时未支付"
        j.orderRepo.Update(ctx, order)
        
        // 2. 释放库存
        j.inventoryClient.ReleaseStock(ctx, order.ReserveIDs)
        
        // 3. 回退优惠券
        if order.CouponID != "" {
            j.marketingClient.ReleaseCoupon(ctx, order.CouponID, order.UserID)
        }
        
        // 4. 发布订单取消事件
        j.eventPublisher.Publish(ctx, &OrderCancelledEvent{
            OrderID: order.OrderID,
            Reason:  "超时未支付",
        })
    }
}

ADR-011: 创单时前后端价格校验策略

决策日期:2026-04-15
状态:已采纳 ✓

问题描述:创单时后端实时查询得到的价格,可能和前端展示的价格不一致(活动变化、价格调整)。应该如何处理这种差异?

决策:采用差异容忍 + 提示机制

价格对比规则

场景差异情况处理策略理由
场景1价格降低✅ 直接通过对用户有利
场景2价格上涨 < 1元✅ 允许(容忍误差)微小差异,可接受
场景3价格上涨 >= 1元 且 < 5%✅ 允许但记录日志合理波动范围
场景4价格上涨 >= 5%❌ 拒绝,要求重新确认差异过大,影响用户决策

实现代码

func (s *OrderService) validatePriceChange(expected, actual int64) error {
    diff := actual - expected
    diffPercent := float64(diff) / float64(expected) * 100
    
    // 场景1: 价格降低 → 允许(对用户有利)
    if diff < 0 {
        return nil
    }
    
    // 场景2: 价格上涨 < 1元 → 允许
    if diff <= 100 {
        return nil
    }
    
    // 场景3: 价格上涨 < 5% → 允许但记录
    if diffPercent < 5.0 {
        log.Warnf("price increased: expected=%d, actual=%d, diff=%d", 
            expected, actual, diff)
        return nil
    }
    
    // 场景4: 价格上涨 >= 5% → 拒绝
    return &PriceChangedError{
        Expected: expected,
        Actual:   actual,
        Message:  fmt.Sprintf("价格已变化:原价%.2f元,现价%.2f元", 
            float64(expected)/100, float64(actual)/100),
    }
}

前端交互

// 前端处理价格变化错误
try {
    const order = await api.createOrder(orderData);
} catch (error) {
    if (error.code === 'PRICE_CHANGED') {
        // 弹窗提示用户
        showConfirmDialog({
            title: '价格已变化',
            message: error.message,
            confirm: '接受新价格并下单',
            cancel: '返回重新选择'
        }).then((confirmed) => {
            if (confirmed) {
                // 用户接受新价格,使用新价格重新下单
                api.createOrder({
                    ...orderData,
                    acceptNewPrice: true,
                    expectedPrice: error.actualPrice
                });
            }
        });
    }
}

ADR-012: 试算价格计算与创单价格计算的统一与差异

决策日期:2026-04-15
状态:已采纳 ✓

问题描述:试算接口(/checkout/calculate)和创单接口(/order/create)都需要计算价格,两者的价格计算逻辑应该如何设计?

决策统一计价服务 + 差异化数据输入

核心设计

接口数据输入计算逻辑快照策略
试算接口可使用快照(5分钟)调用统一计价服务允许快照数据
创单接口强制实时查询调用统一计价服务禁止快照数据

理由

  1. 计价逻辑统一

    • 试算和创单使用同一个 PricingService.Calculate
    • 避免"试算价格"与"订单价格"不一致
    • 营销规则变更只需更新一处
  2. 数据输入差异化

    • 试算:允许使用缓存/快照数据(性能优先)
    • 创单:强制实时查询(准确性优先)
  3. 最终一致性保证

    • 试算阶段可能使用过期快照
    • 创单阶段的实时查询是最后防线
    • 价格差异会被拦截并提示用户

架构图

graph TB
    subgraph 试算接口
        A1[Checkout.Calculate]
        A2[使用快照数据<br/>性能优先]
    end
    
    subgraph 创单接口
        B1[Order.Create]
        B2[强制实时查询<br/>准确性优先]
    end
    
    subgraph 计价服务
        C[PricingService.Calculate<br/>统一计算逻辑]
    end
    
    A1 --> A2
    A2 --> C
    B1 --> B2
    B2 --> C

ADR-013: 价格在整个交易链路中的流转与计算策略

决策日期:2026-04-15
状态:已采纳 ✓

问题描述:从用户搜索商品到最终支付,价格会经历多个阶段(搜索列表 → 商品详情 → 加购试算 → 创单 → 支付)。每个阶段的价格计算范围、数据来源、系统交互都不同。需要一个全局视角来理解价格是如何流转的,以及各阶段的相同点和不同点。

核心挑战

业务困惑:
• 为什么搜索列表的价格和详情页不一样?
• 详情页显示的价格和试算价格能保证一致吗?
• 试算价格和最终支付价格可能不同吗?
• 每个阶段都要调用Pricing Service吗?
• 基础价格、营销折扣、优惠券、Coin、支付渠道费分别在哪个阶段计算?

决策:采用**"分阶段计算 + 逐步扩展价格维度 + 最终强制校验"**策略


价格流转全局图

用户旅程:搜索 → 详情 → 试算 → 创单 → 支付
           ↓      ↓      ↓      ↓      ↓
价格计算: 基础价  +营销  +营销  +营销  +Coin+Voucher+渠道费
           ↓      ↓      ↓      ↓      ↓
数据来源: ES缓存  实时   快照   强制   强制实时
                         (可选) 实时
           ↓      ↓      ↓      ↓      ↓
性能目标: 30ms   150ms  230ms  500ms  200ms

五个阶段对比

阶段价格维度数据来源性能目标计算复杂度资损风险
搜索列表基础价(最低价)ES缓存(延迟1-5分钟)P95 < 30ms低(只查ES)
商品详情基础价 + 营销折扣实时查询 + 生成快照P95 < 150ms中(3个服务)
结算试算基础价 + 营销 + 数量快照 OR 实时查询P95 < 230ms中(可能3个服务)
确认下单基础价 + 营销 + 数量 + 券强制实时查询P95 < 500ms高(4个服务 + 预占)
支付确认上述 + Coin + Voucher + 渠道费强制实时查询P95 < 200ms高(多维度计算)极高

核心设计原则

  1. 逐步扩展价格维度

    搜索:最低价(吸引用户)
    详情:折扣价(展示营销)
    试算:总价(含数量、券)
    创单:锁定价(预占资源)
    支付:最终价(含所有优惠与费用)
    
  2. 数据来源分级

    搜索/详情:允许缓存(性能优先)
    试算:允许快照(性能与准确性平衡)
    创单/支付:强制实时(安全优先)
    
  3. 多道防线保证准确性

    详情页:生成快照(用于试算)
    试算:对比快照与实时(发现变化)
    创单:强制实时 + 价格校验(最后防线)
    支付:二次校验 + Coin/Voucher锁定(终极防线)
    

监控指标

  • 各阶段P95响应时间
  • 快照命中率(目标 > 80%)
  • 价格差异率(试算vs创单,目标 < 5%)
  • 价格变化拦截率(创单价格校验触发频率)

16.6 系统边界与集成实践

16.6.1 边界划分的实际案例

案例1:计价系统的边界重构

初始问题

  • 价格计算逻辑分散在订单、营销、商品三个域
  • 购物车、订单创建、支付确认三处价格计算不一致
  • 无法支持"PDP加购试算"场景

重构方案

  1. 新建计价上下文:职责是提供统一的试算接口
  2. 定义边界
    • 计价上下文不拥有商品基础价、营销规则、订单状态
    • 对外提供 Calculate(items, promotions, context) -> PriceBreakdown
    • 各场景通过统一接口获取价格
  3. 收益
    • 价格一致性得到保证
    • 营销规则变更只需在营销域发布事件
    • 支持了试算、价格预览、价格审计等新需求

案例2:库存预占的归属

争议:库存预占应该放在订单域还是库存域?

决策:放在库存域

理由

  • 库存域拥有库存数据所有权
  • 预占是库存的一种状态(可售 → 预占 → 扣减)
  • 订单域只需调用库存域的 Reserve 接口
  • 降低耦合:订单域不需要了解库存的存储结构

16.6.2 集成模式选择

集成场景模式理由
订单 → 商品同步RPC需要实时获取商品信息,延迟<100ms
订单 → 库存同步RPC库存预占是核心路径,必须同步
订单 → 支付同步RPC支付创建需要同步返回支付URL
订单成功 → 搜索异步事件销量更新非核心路径,可最终一致
订单成功 → 积分异步事件积分增加非核心路径

事件驱动示例

// 订单域发布事件
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    // 创建订单...
    order := &Order{...}
    s.repo.Save(ctx, order)
    
    // 发布事件(Outbox模式)
    event := &OrderCreatedEvent{
        OrderID:    order.ID,
        UserID:     order.UserID,
        TotalPrice: order.TotalPrice,
        Items:      order.Items,
    }
    s.outbox.Publish(ctx, "order-events", event)
    
    return order, nil
}

// 搜索域订阅事件
func (s *SearchService) HandleOrderCreated(ctx context.Context, event *OrderCreatedEvent) error {
    // 更新商品销量(用于排序)
    for _, item := range event.Items {
        s.incrementSales(ctx, item.SkuID, item.Quantity)
    }
    return nil
}

16.6.3 跨系统事务处理

Saga模式(编排)

// 订单创建Saga
type CreateOrderSaga struct {
    inventoryClient rpc.InventoryClient
    marketingClient rpc.MarketingClient
    orderRepo       *OrderRepo
}

func (s *CreateOrderSaga) Execute(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    var reserveID string
    var couponLockID string
    
    // Step 1: 库存预占
    reserve, err := s.inventoryClient.ReserveStock(ctx, req.Items)
    if err != nil {
        return nil, fmt.Errorf("库存预占失败: %w", err)
    }
    reserveID = reserve.ReserveID
    defer func() {
        if err != nil {
            // 补偿:释放库存
            s.inventoryClient.ReleaseStock(ctx, reserveID)
        }
    }()
    
    // Step 2: 优惠券锁定
    couponLock, err := s.marketingClient.LockCoupon(ctx, req.CouponCode, req.UserID)
    if err != nil {
        return nil, fmt.Errorf("优惠券锁定失败: %w", err)
    }
    couponLockID = couponLock.LockID
    defer func() {
        if err != nil {
            // 补偿:释放优惠券
            s.marketingClient.UnlockCoupon(ctx, couponLockID)
        }
    }()
    
    // Step 3: 创建订单
    order := &Order{
        ID:           generateOrderID(),
        UserID:       req.UserID,
        Items:        req.Items,
        ReserveID:    reserveID,
        CouponLockID: couponLockID,
        Status:       StatusPendingPayment,
    }
    err = s.orderRepo.Save(ctx, order)
    if err != nil {
        return nil, fmt.Errorf("订单创建失败: %w", err)
    }
    
    return order, nil
}

16.6.4 集成层设计

防腐层(Anti-Corruption Layer)

// 供应商响应模型(外部)
type SupplierFlightResponse struct {
    Code    string  `json:"code"`
    Message string  `json:"message"`
    Data    struct {
        FlightNo  string  `json:"flight_no"`
        Available int     `json:"available"`
        Price     float64 `json:"price"`
    } `json:"data"`
}

// 平台库存模型(内部)
type StockResponse struct {
    Available bool
    Quantity  int
    Message   string
}

// 防腐层:翻译外部模型 → 内部模型
func (a *FlightSupplierACL) TranslateStock(supplierResp *SupplierFlightResponse) *StockResponse {
    return &StockResponse{
        Available: supplierResp.Code == "SUCCESS" && supplierResp.Data.Available > 0,
        Quantity:  supplierResp.Data.Available,
        Message:   supplierResp.Message,
    }
}

收益

  • 领域层不被供应商模型污染
  • 供应商接口变更时,修改集中在ACL
  • 测试时可以使用Fake实现替代真实供应商

16.7 高可用与性能优化

16.7.1 高可用设计

服务多副本部署

服务正常副本大促副本扩容策略
Product Center618CPU > 70% 自动扩容
Inventory618QPS > 5000 扩容
Order824QPS > 3000 扩容
Payment412QPS > 2000 扩容

数据库高可用

MySQL:
• 主从复制(1主2从)
• 双主互备(支付库)
• 自动故障转移(MHA)

Redis:
• Sentinel模式(1主2从3哨兵)
• 自动故障转移

Kafka:
• 3副本
• ISR机制

熔断与降级

// 熔断配置
type CircuitBreakerConfig struct {
    MaxRequests       uint32        // 半开状态最大请求数
    Interval          time.Duration // 统计窗口
    Timeout           time.Duration // 熔断超时时间
    FailureThreshold  float64       // 失败率阈值(0-1)
}

// 降级策略
func (s *SearchAggregation) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
    // 尝试调用Marketing Service
    promos, err := s.marketingClient.GetPromotions(ctx, req.SkuIDs)
    if err != nil {
        // 降级:使用基础价格(不展示营销信息)
        log.Warn("Marketing Service故障,降级为基础价格")
        promos = make(map[int64]*PromoInfo)  // 空促销
    }
    
    // 继续后续流程...
    return s.buildResponse(products, promos)
}

16.7.2 性能优化

缓存策略(多级缓存):

// L1: 本地缓存(进程内)
type LocalCache struct {
    cache *bigcache.BigCache
}

func (c *LocalCache) Get(key string) (interface{}, error) {
    data, err := c.cache.Get(key)
    if err == nil {
        return unmarshal(data), nil
    }
    return nil, err
}

// L2: Redis缓存
// L3: MySQL数据库

func (s *ProductService) GetProduct(ctx context.Context, skuID int64) (*Product, error) {
    // L1: 本地缓存
    if product, err := s.localCache.Get(skuID); err == nil {
        return product, nil
    }
    
    // L2: Redis缓存
    if product, err := s.redis.Get(ctx, fmt.Sprintf("product:%d", skuID)); err == nil {
        s.localCache.Set(skuID, product)  // 回填L1
        return product, nil
    }
    
    // L3: MySQL数据库
    product, err := s.repo.GetByID(ctx, skuID)
    if err != nil {
        return nil, err
    }
    
    // 回填缓存
    s.redis.Set(ctx, fmt.Sprintf("product:%d", skuID), product, 30*time.Minute)
    s.localCache.Set(skuID, product)
    
    return product, nil
}

批量查询优化

// 批量获取商品信息(减少RPC调用)
func (s *ProductService) BatchGetProducts(ctx context.Context, skuIDs []int64) (map[int64]*Product, error) {
    // Step 1: 尝试从缓存批量获取
    cached := s.redis.MGet(ctx, toCacheKeys(skuIDs))
    
    // Step 2: 找出缺失的ID
    missingIDs := findMissing(skuIDs, cached)
    
    // Step 3: 批量查询数据库(IN查询)
    if len(missingIDs) > 0 {
        missing, _ := s.repo.GetByIDs(ctx, missingIDs)
        // 回填缓存
        s.redis.MSet(ctx, missing, 30*time.Minute)
        cached = merge(cached, missing)
    }
    
    return cached, nil
}

数据库优化

-- 索引优化
CREATE INDEX idx_order_user_create ON `order` (user_id, create_time DESC);
CREATE INDEX idx_order_status ON `order` (status, create_time DESC);

-- 避免SELECT *(只查询需要的字段)
SELECT order_id, status, total_price FROM `order` WHERE user_id = ?;

-- 分页优化(使用索引覆盖)
SELECT order_id FROM `order` 
WHERE user_id = ? AND create_time > ?
ORDER BY create_time DESC
LIMIT 20;

16.7.3 容灾与降级

多机房部署

Region A(主):
• 写流量:100%
• 读流量:70%

Region B(备):
• 写流量:0%(只读副本)
• 读流量:30%

灾难切换:
• 自动故障检测(3秒)
• 流量切换到Region B(30秒)
• RTO:< 2分钟

降级开关

// Feature Flag控制降级
func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
    // 检查Feature Flag
    if s.featureFlag.IsEnabled(ctx, "marketing.enabled") {
        // 正常逻辑:调用Marketing Service
        promos, _ := s.marketingClient.GetPromotions(ctx, req)
        return s.calculateWithPromos(req, promos)
    } else {
        // 降级逻辑:不使用营销信息
        return s.calculateBasic(req)
    }
}

16.8 团队组织与协作

16.8.1 团队结构

康威定律实践:系统架构反映组织沟通结构。

订单团队(15人)
├─ 订单核心(5人):订单创建、状态机
├─ 订单查询(3人):我的订单、订单详情
├─ 履约对接(4人):供应商履约、异常处理
└─ 测试(3人)

商品团队(12人)
├─ 商品中心(6人):SPU/SKU管理
├─ 类目属性(3人):类目树、属性模板
└─ 测试(3人)

库存团队(10人)
├─ 库存核心(5人):预占、扣减、释放
├─ 供应商同步(3人):实时查询、定时同步
└─ 测试(2人)

跨团队协作

场景协作方式工具
API契约OpenAPI/Proto定义Swagger、Buf
事件契约Schema RegistryConfluent Schema Registry
联调测试契约测试Pact
故障处理On-call轮值PagerDuty

16.8.2 协作流程

需求评审流程

1. 产品提需求(PRD)
   ↓
2. 技术评审(架构师+各团队Lead)
   • 是否需要新增服务?
   • 是否需要修改API契约?
   • 是否需要数据库迁移?
   ↓
3. API契约评审(上下游团队)
   • 定义Request/Response
   • 明确超时、重试策略
   • 确认降级方案
   ↓
4. 开发排期
   • 各团队独立开发
   • 契约测试通过后联调
   ↓
5. 集成测试
   • 端到端测试
   • 性能测试
   ↓
6. 灰度发布
   • 5% → 20% → 50% → 100%

实际案例:新增"拼团"功能的完整协作流程

第1周:需求评审与技术方案

【产品需求】
- 用户发起拼团(3人成团,24小时有效)
- 拼团价格比正常价格低20%
- 成团后统一发货,不成团退款

【技术评审会议】(2小时,架构师+6个团队Lead)
问题1:拼团功能是否需要新增服务?
  → 决策:新增"拼团服务"(GroupBuy Service)
  → 理由:拼团逻辑复杂(成团判断、超时处理),独立服务便于维护

问题2:拼团价格如何计算?
  → 决策:在Pricing Service中新增"拼团价格策略"
  → 理由:价格计算逻辑应该统一管理

问题3:拼团成功后如何扣减库存?
  → 决策:拼团成功时批量预占库存(3人份)
  → 理由:避免成团后库存不足

【输出物】
- 技术方案文档(15页)
- 服务依赖图(Mermaid图)
- 数据库设计(ER图)
- 时序图(成团流程、超时处理)

第2周:API契约评审

【API契约】
// 创建拼团
POST /groupbuy/create
Request:
{
  "sku_id": 1001,
  "original_price": 299.00,
  "groupbuy_price": 239.00,  // 8折
  "required_count": 3,        // 3人成团
  "expire_hours": 24          // 24小时有效
}
Response:
{
  "groupbuy_id": "GB20260501123456",
  "status": "waiting",        // 等待中
  "current_count": 1,         // 当前人数
  "required_count": 3,
  "expires_at": 1744633200
}

// 参与拼团
POST /groupbuy/join
Request:
{
  "groupbuy_id": "GB20260501123456",
  "user_id": 67890
}
Response:
{
  "status": "success",        // 成功 or 团满
  "order_id": "ORD123456",    // 如果成团,返回订单号
  "current_count": 3
}

【契约测试】
- 上游:前端团队(Web、App)
- 下游:Pricing Service、Inventory Service、Order Service
- 测试工具:Pact
- 测试覆盖:100%(所有API)

第3-4周:并行开发

【团队分工】
拼团团队(5人):
  - 拼团服务核心逻辑
  - 超时任务(15分钟扫描一次)
  - 数据库表设计(groupbuy、groupbuy_participant)

计价团队(2人):
  - 新增拼团价格策略
  - 拼团价格校验

库存团队(2人):
  - 批量预占库存接口

订单团队(3人):
  - 拼团成团后批量创建订单
  - 拼团失败后退款

前端团队(4人):
  - 拼团页面(发起、参与、分享)
  - 倒计时组件

【每日站会】(15分钟)
- 各团队汇报进度
- 识别阻塞点
- 协调资源

【契约测试通过率】
- 第3周末:70%
- 第4周末:100% ✅

第5周:集成测试

【测试场景】
场景1:正常成团
  1. 用户A发起拼团(3人成团)
  2. 用户B、C参与拼团
  3. 成团 → 创建3个订单 → 预占库存(3份)
  4. 用户A、B、C支付 → 确认扣减库存

场景2:超时未成团
  1. 用户A发起拼团(3人成团)
  2. 只有用户B参与(2人)
  3. 24小时后超时 → 标记拼团失败 → 退款

场景3:库存不足
  1. 用户A发起拼团(3人成团)
  2. 用户B、C参与拼团
  3. 成团时库存不足(只剩2个)→ 拼团失败 → 退款

【性能测试】
- 并发创建拼团:1000 TPS
- 并发参与拼团:5000 TPS
- 超时扫描任务:1000个拼团/秒
- P99延迟:< 300ms ✅

第6周:灰度发布

【灰度策略】
阶段1(5%):内部员工 + 白名单用户(1000人)
  → 观察1天:成团率、退款率、投诉数

阶段2(20%):北京、上海用户
  → 观察3天:性能指标、业务指标

阶段3(50%):全国用户
  → 观察1周

阶段4(100%):全量发布
  → 持续监控1个月

【关键指标】
- 成团率:65%(目标 > 60%)✅
- 退款率:5%(目标 < 10%)✅
- 用户投诉:3起/天(目标 < 10起)✅
- P99延迟:280ms(目标 < 300ms)✅

协作关键点

阶段关键协作点工具/机制
需求评审架构师+各团队Lead对齐技术方案会议+文档
API契约上下游团队明确接口定义OpenAPI + Pact
并行开发各团队独立开发,通过契约测试联调Pact + Mock Server
集成测试端到端测试,验证完整流程自动化测试平台
灰度发布分阶段发布,持续监控Feature Flag + Grafana

变更管理

// ADR(Architecture Decision Record)
// 记录重大架构决策

## ADR-014: 拼团功能是否复用订单服务

**决策日期**:2026-05-01
**状态**:已采纳 ✓

**问题描述**:
拼团功能需要创建订单,是在订单服务中新增拼团逻辑,还是新建拼团服务?

**备选方案**:
A. 在订单服务中新增拼团逻辑
   ✓ 复用订单创建逻辑
   ✗ 订单服务变得臃肿
   ✗ 拼团逻辑与订单逻辑耦合

B. 新建拼团服务
   ✓ 拼团逻辑独立,便于维护
   ✓ 订单服务保持单一职责
   ✗ 需要新建服务(增加运维成本)

**决策**:采用方案B,新建拼团服务

**理由**:
1. 拼团逻辑复杂(成团判断、超时处理、退款逻辑)
2. 拼团是营销活动,不是订单核心流程
3. 未来可能有"砍价""秒杀"等类似活动,独立服务便于扩展

**影响范围**:
- 新增服务:GroupBuy Service
- QPS估算:2000(正常)/ 10000(大促)
- 部署规模:4副本(正常)/ 12副本(大促)

**后续行动**:
- ✓ 已完成:GroupBuy Service开发
- ✓ 已完成:与订单服务集成
- ✓ 已完成:灰度上线

16.8.3 技术治理

代码评审清单

  • 是否符合分层架构(依赖方向正确)
  • 是否有单元测试(覆盖率 > 80%)
  • 是否有集成测试(核心路径)
  • 是否有性能测试(Benchmark)
  • 是否有监控指标(Prometheus Metrics)
  • 是否有日志(结构化日志)
  • 是否有文档(API文档、设计文档)
  • 是否考虑降级方案

技术债管理

## 技术债清单

| 优先级 | 类型 | 描述 | 负责人 | 预计工作量 |
|-------|------|------|--------|-----------|
| P0 | 性能 | 订单查询慢查询优化 | @张三 | 2天 |
| P1 | 安全 | 支付回调签名验证 | @李四 | 1天 |
| P2 | 代码 | 商品中心重复代码重构 | @王五 | 3天 |

16.9 上线与演进

16.9.1 上线策略

分阶段上线

阶段1:基础功能(2周)
• 商品中心、库存服务、订单服务
• 支持机票、酒店两个品类
• 单机房部署

阶段2:营销功能(2周)
• 营销服务、计价服务
• 支持优惠券、活动

阶段3:新品类(每周1个)
• 充值、电影票、优惠券、礼品卡

阶段4:多机房(4周)
• 双机房部署
• 流量灰度切换

16.9.2 灰度发布

灰度策略

// 灰度规则
type GrayReleaseRule struct {
    Version    string   // 新版本号
    Percentage int      // 流量比例(0-100)
    Whitelist  []int64  // 白名单用户ID
    Regions    []string // 灰度地区
}

func (r *GrayRouter) Route(userID int64, region string) string {
    // 白名单用户直接路由到新版本
    if contains(r.rule.Whitelist, userID) {
        return r.rule.Version
    }
    
    // 按地区灰度
    if !contains(r.rule.Regions, region) {
        return "stable"  // 老版本
    }
    
    // 按百分比灰度
    if hash(userID) % 100 < r.rule.Percentage {
        return r.rule.Version  // 新版本
    }
    
    return "stable"  // 老版本
}

灰度步骤

1. 5%流量(白名单用户 + 内部员工)
   观察1小时:错误率、延迟、业务指标

2. 20%流量(特定地区)
   观察2小时

3. 50%流量
   观察4小时

4. 100%流量(全量发布)
   观察24小时

5. 下线老版本

16.9.3 监控告警

三级监控体系

层级监控对象工具告警阈值
业务监控订单量、GMV、转化率Grafana + ClickHouse同比下降20%
应用监控QPS、延迟、错误率Prometheus + GrafanaP99延迟>500ms
基础设施监控CPU、内存、磁盘、网络Prometheus + Node ExporterCPU>80%

核心指标

// Prometheus Metrics
package metrics

import "github.com/prometheus/client_golang/prometheus"

var (
    // 业务指标
    orderCreatedTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "order_created_total",
            Help: "订单创建总数",
        },
        []string{"category", "status"},  // 标签:品类、状态
    )
    
    orderCreatedLatency = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "order_created_latency_seconds",
            Help:    "订单创建延迟",
            Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
        },
        []string{"category"},
    )
    
    orderGMV = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "order_gmv_total",
            Help: "订单GMV(元)",
        },
        []string{"date"},
    )
    
    // 系统指标
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP请求延迟",
            Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
        },
        []string{"method", "endpoint", "status"},
    )
    
    httpRequestTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_request_total",
            Help: "HTTP请求总数",
        },
        []string{"method", "endpoint", "status"},
    )
    
    // 依赖服务指标
    rpcCallDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "rpc_call_duration_seconds",
            Help:    "RPC调用延迟",
            Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5},
        },
        []string{"service", "method", "status"},
    )
    
    rpcCallTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "rpc_call_total",
            Help: "RPC调用总数",
        },
        []string{"service", "method", "status"},
    )
    
    // 数据库指标
    dbQueryDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "db_query_duration_seconds",
            Help:    "数据库查询延迟",
            Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1},
        },
        []string{"query_type", "table"},
    )
    
    // Redis指标
    redisCommandDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "redis_command_duration_seconds",
            Help:    "Redis命令延迟",
            Buckets: []float64{.0001, .0005, .001, .005, .01, .025, .05, .1},
        },
        []string{"command"},
    )
)

// 使用示例
func CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    startTime := time.Now()
    
    // 业务逻辑...
    order, err := createOrderInternal(ctx, req)
    
    // 记录指标
    duration := time.Since(startTime).Seconds()
    category := req.Category
    status := "success"
    if err != nil {
        status = "failed"
    }
    
    // 记录订单创建总数
    orderCreatedTotal.WithLabelValues(category, status).Inc()
    
    // 记录订单创建延迟
    orderCreatedLatency.WithLabelValues(category).Observe(duration)
    
    // 记录GMV
    if err == nil {
        orderGMV.WithLabelValues(time.Now().Format("2006-01-02")).Add(float64(order.TotalPrice))
    }
    
    return order, err
}

告警规则

# Prometheus AlertManager规则
groups:
  - name: order-service-alerts
    rules:
      # P99延迟告警
      - alert: OrderCreateLatencyHigh
        expr: histogram_quantile(0.99, order_created_latency_seconds) > 1
        for: 5m
        labels:
          severity: warning
          service: order-service
        annotations:
          summary: "订单创建延迟过高"
          description: "P99延迟 {{ $value }}s > 1s(持续5分钟)"
          dashboard: "https://grafana.example.com/d/order-service"
      
      # 错误率告警
      - alert: OrderCreateErrorRateHigh
        expr: |
          sum(rate(order_created_total{status="failed"}[5m])) 
          / sum(rate(order_created_total[5m])) > 0.01
        for: 5m
        labels:
          severity: critical
          service: order-service
        annotations:
          summary: "订单创建失败率过高"
          description: "失败率 {{ $value | humanizePercentage }} > 1%"
          runbook: "https://wiki.example.com/runbook/order-create-error"
      
      # QPS下降告警(业务异常)
      - alert: OrderCreateQPSDrop
        expr: |
          (sum(rate(order_created_total[5m])) 
          / sum(rate(order_created_total[5m] offset 1h))) < 0.5
        for: 10m
        labels:
          severity: warning
          service: order-service
        annotations:
          summary: "订单创建QPS骤降"
          description: "当前QPS {{ $value }},比1小时前下降50%以上"
      
      # GMV下降告警(业务异常)
      - alert: OrderGMVDrop
        expr: |
          (sum(rate(order_gmv_total[1h])) 
          / sum(rate(order_gmv_total[1h] offset 24h))) < 0.8
        for: 30m
        labels:
          severity: critical
          service: order-service
        annotations:
          summary: "订单GMV大幅下降"
          description: "当前GMV {{ $value }},比昨天同期下降20%以上"
      
      # RPC调用失败率告警
      - alert: RPCCallErrorRateHigh
        expr: |
          sum(rate(rpc_call_total{status!="success"}[5m])) by (service) 
          / sum(rate(rpc_call_total[5m])) by (service) > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "RPC调用失败率过高:{{ $labels.service }}"
          description: "失败率 {{ $value | humanizePercentage }} > 5%"
      
      # 数据库慢查询告警
      - alert: DBSlowQuery
        expr: histogram_quantile(0.99, db_query_duration_seconds) > 0.1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "数据库慢查询"
          description: "P99延迟 {{ $value }}s > 100ms"
      
      # Redis延迟告警
      - alert: RedisLatencyHigh
        expr: histogram_quantile(0.99, redis_command_duration_seconds) > 0.01
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Redis延迟过高"
          description: "P99延迟 {{ $value }}s > 10ms"
      
      # 服务实例Down告警
      - alert: ServiceInstanceDown
        expr: up{job="order-service"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "服务实例宕机"
          description: "实例 {{ $labels.instance }} 已宕机超过1分钟"

告警分级与处理

级别触发条件通知方式响应时间处理人
P0(紧急)GMV下降>20%、服务全部宕机电话+短信+企业微信< 5分钟On-call工程师+经理
P1(严重)错误率>1%、P99延迟>1s企业微信+短信< 15分钟On-call工程师
P2(警告)QPS下降>50%、数据库慢查询企业微信< 30分钟值班工程师
P3(提示)磁盘使用>80%、内存使用>80%邮件< 2小时运维团队

监控大屏

┌─────────────────────────────────────────────────────────┐
│                   订单服务实时监控大屏                    │
├─────────────────────────────────────────────────────────┤
│  今日订单量: 1,234,567  ↑ 12.3%   今日GMV: ¥456,789,012  │
│  当前QPS: 2,345         P99延迟: 234ms    错误率: 0.12%  │
├─────────────────────────────────────────────────────────┤
│  订单创建趋势(24小时)             QPS & P99延迟         │
│  ███████████████████████████████   ███████████████████   │
│  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒     │
│  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░░     │
├─────────────────────────────────────────────────────────┤
│  品类分布               服务依赖健康度                   │
│  机票: 45%  ████████   Product Service:   ✅ 正常        │
│  酒店: 30%  ██████     Inventory Service: ✅ 正常        │
│  充值: 15%  ███        Pricing Service:   ⚠️  延迟高     │
│  其他: 10%  ██         Marketing Service: ✅ 正常        │
├─────────────────────────────────────────────────────────┤
│  活跃告警(3条)                                         │
│  ⚠️  P1 Pricing Service P99延迟>500ms(持续10分钟)      │
│  📊 P2 订单QPS比昨天同期下降15%                          │
│  💾 P3 MySQL主库连接数>80%                              │
└─────────────────────────────────────────────────────────┘

On-call值班机制

【值班表】(7x24小时)
周一:张三(订单团队)
周二:李四(商品团队)
周三:王五(库存团队)
...

【值班职责】
1. 响应P0/P1告警(5分钟内)
2. 排查问题根因(15分钟内定位)
3. 协调资源修复(30分钟内恢复)
4. 事后复盘(24小时内)

【升级机制】
On-call工程师无法处理 → 升级到Team Lead
Team Lead无法处理 → 升级到架构师
架构师无法处理 → 升级到CTO

16.9.4 系统演进路径

已完成

  • ✅ 基础架构搭建(微服务、服务发现、监控)
  • ✅ 核心品类上线(机票、酒店、充值)
  • ✅ 营销系统(优惠券、活动)
  • ✅ 双机房部署

进行中

  • 🚧 性能优化(P99延迟 < 200ms)
  • 🚧 新品类接入(电影票、礼品卡)
  • 🚧 供应商扩展(50+ → 100+)

规划中

  • 📅 国际化(多语言、多币种)
  • 📅 推荐系统(AI推荐)
  • 📅 智能客服(NLP)
  • 📅 区块链溯源(高端商品)

16.10 经验总结

16.10.1 成功经验

1. 架构决策记录(ADR)制度

价值:

  • 重大决策留痕,新人可快速了解背景
  • 避免重复讨论已解决的问题
  • 架构演进有据可查

建议:

  • 每个ADR包含:问题、决策、理由、权衡、影响范围
  • 定期Review(每季度)
  • 与代码一起版本管理

2. 品类差异化设计

价值:

  • 避免"一刀切"架构(机票与充值差异大)
  • 策略模式让新品类接入成本降低80%
  • 适配器模式让供应商集成周期从4周缩短到1周

建议:

  • 先分析业务模型差异,再设计技术方案
  • 抽象共性,策略处理差异
  • 避免过度抽象(YAGNI原则)

3. 聚合层编排模式

价值:

  • API Gateway职责单一(鉴权、限流、路由)
  • 业务编排集中在聚合层,易于优化
  • 降级策略统一管理

建议:

  • 聚合层只做数据获取与编排,不做业务计算
  • 支持并发调用(提升性能)
  • 统一降级策略(Marketing故障降级为基础价)

4. 多级缓存策略

价值:

  • P99延迟从500ms降低到200ms
  • Redis QPS降低60%(本地缓存命中率30%)
  • 大促期间扛住5倍流量

建议:

  • L1(本地):热点数据,1分钟TTL
  • L2(Redis):通用数据,30分钟TTL
  • L3(MySQL):源数据
  • 缓存失效策略:主动失效 + TTL兜底

5. 契约测试

价值:

  • 上下游团队并行开发(不等联调)
  • API变更影响提前发现
  • 集成测试成本降低70%

建议:

  • 使用Pact等契约测试工具
  • API契约与代码一起版本管理
  • CI自动运行契约测试

16.10.2 踩过的坑

坑1:过早引入Event Sourcing

问题

  • 初期为了"追求架构完美"引入Event Sourcing
  • 团队对ES理解不足,查询复杂,运维困难
  • 投影重建耗时长(大促后修复bug需要重建投影,耗时4小时)

教训

  • Event Sourcing不是银弹,适用于审计要求极高的场景
  • 对于大部分电商场景,CQRS(不带ES)足够
  • 先用简单方案(CRUD),待确认瓶颈后再演进

坑2:供应商接口未做熔断

问题

  • 某供应商故障,接口超时(30秒)
  • 大量请求堆积,线程池耗尽
  • 整个订单服务不可用(影响其他供应商)

教训

  • 所有外部调用必须熔断(gobreaker)
  • 超时时间合理设置(不超过1秒)
  • 故障隔离(某个供应商故障不影响其他)

坑3:分库分表过早

问题

  • 订单量100万时就分库分表(8库64表)
  • 运维复杂度激增(扩容、迁移、对账)
  • 跨库查询需要路由表,增加延迟

教训

  • 单表500万以下不分表(MySQL性能足够)
  • 单库3000万以下不分库
  • 分库分表需要充分评估成本收益

坑4:忽视数据一致性对账

问题

  • 库存预占后未释放(代码bug)
  • 累积1个月后,库存数据严重不准确
  • 影响用户体验(明明有库存却提示"已售罄")

教训

  • 异步操作必须有对账机制(每小时/每天)
  • 对账发现差异要有自动补偿
  • 监控库存准确率(定期抽查)

坑5:缓存穿透导致雪崩

问题

  • 恶意请求查询不存在的商品(skuID=0)
  • 缓存未命中,直接打到数据库
  • 数据库连接池耗尽,服务雪崩

教训

  • 布隆过滤器(Bloom Filter)拦截不存在的Key
  • 缓存空值(TTL=1分钟)
  • 请求参数校验(前置拦截非法请求)

16.10.3 改进方向

短期改进(3个月内)

  1. 性能优化

    • 目标:P99延迟从200ms降低到150ms
    • 措施
      • 热点数据预加载:大促前提前加载10万+热门商品到Redis
      • 数据库慢查询优化:全部慢查询(<50ms),添加复合索引
      • 连接池优化:MySQL连接池从100提升到500
      • 批量查询优化:单次查询支持100+商品(原50个)
    • 预期收益:QPS提升30%,响应时间降低25%
  2. 稳定性提升

    • 混沌工程实践
      • 每周定期故障演练(随机Kill Pod、网络延迟、数据库主从切换)
      • 自动化故障注入工具(Chaos Mesh)
      • 故障恢复时间目标:< 3分钟
    • 降级开关完善
      • 所有非核心功能支持降级(营销、推荐、评论)
      • Feature Flag平台(实时开关,无需重启)
      • 降级决策自动化(根据错误率自动降级)
    • 容量规划
      • 提前3个月预估资源需求(基于历史数据+增长率)
      • 大促前1个月进行压测(验证容量)
      • 弹性扩容策略(CPU > 70%自动扩容)
  3. 开发效率

    • 统一脚手架
      • 一键创建新服务(包含标准目录结构、配置文件、CI/CD)
      • 内置最佳实践(监控、日志、链路追踪)
      • 代码生成工具(Proto → Go代码自动生成)
    • 自动化测试
      • 单元测试覆盖率 > 90%(核心业务逻辑100%覆盖)
      • 集成测试自动化(每次提交自动运行)
      • 性能测试定期执行(每周一次,P99延迟不能退化)
    • CI/CD优化
      • 构建时间 < 5分钟(并行构建、增量构建、缓存优化)
      • 自动化部署(合并到main分支自动部署到生产)
      • 灰度发布流程标准化(5% → 20% → 50% → 100%)

中期改进(6-12个月)

  1. 智能化

    • 推荐系统

      • 协同过滤(基于用户行为相似度)
      • 深度学习模型(基于用户画像+商品属性)
      • 实时推荐(用户浏览行为实时调整推荐结果)
      • A/B测试(对比推荐效果,持续优化)
      • 预期提升:点击率+15%,转化率+10%
    • 动态定价

      • 根据供需关系自动调价(库存少+需求高 → 涨价)
      • 竞品价格监控(爬虫+算法,自动调整价格)
      • 用户画像定价(VIP用户优惠力度更大)
      • 时段定价(早上价格高,晚上价格低)
      • 预期提升:毛利率+8%,订单量+12%
    • 智能客服

      • FAQ自动回复(NLP模型识别用户问题)
      • 订单查询自动化(用户输入订单号,自动查询状态)
      • 售后自动化(退款、换货流程自动化)
      • 人工客服辅助(AI推荐回复话术)
      • 预期收益:客服成本降低40%,响应速度提升50%
  2. 国际化

    • 多语言支持(i18n)

      • 支持英语、中文、日语、韩语、泰语
      • 翻译管理平台(统一管理翻译资源)
      • 动态语言切换(用户可随时切换语言)
      • 本地化适配(日期格式、货币符号、文化差异)
    • 多币种支持

      • 支持USD、EUR、JPY、CNY等10+币种
      • 汇率实时转换(接入外汇API,每分钟更新)
      • 价格展示优化(根据用户地区自动选择币种)
      • 结算币种选择(支持多币种支付)
    • 跨境支付

      • 接入PayPal、Stripe(国际信用卡)
      • 本地化支付(日本:Pay-easy,韩国:KakaoPay)
      • 外汇结算(自动结汇,降低汇率风险)
  3. 数据驱动

    • 实时数据大屏

      • GMV实时展示(今日/本周/本月)
      • 订单量、转化率、客单价实时监控
      • 品类TOP10、商品TOP100
      • 地域分布、用户画像
      • 技术栈:Flink + ClickHouse + Grafana
    • A/B测试平台

      • 灰度实验(新功能A/B测试)
      • 流量分配(按用户ID哈希,保证一致性)
      • 效果评估(点击率、转化率、收入对比)
      • 自动化决策(效果好的方案自动全量)
    • 用户画像

      • 行为标签(浏览、加购、下单、复购)
      • 偏好标签(品类偏好、价格敏感度、优惠敏感度)
      • 生命周期标签(新用户、活跃用户、流失用户)
      • 精准营销(根据画像推送个性化优惠)

长期愿景(1-3年)

  1. 平台化

    • 开放API

      • 商品API(第三方接入商品数据)
      • 订单API(第三方接入订单流程)
      • 支付API(第三方接入支付能力)
      • API网关(统一鉴权、限流、监控)
      • 预期收益:生态规模扩大3倍
    • SaaS化

      • 中小企业独立部署(提供SaaS服务)
      • 多租户隔离(数据隔离、资源隔离)
      • 按需付费(按订单量或GMV收费)
      • 自助配置(商家自助配置商品、营销)
    • 生态建设

      • 开发者社区(技术文档、SDK、Demo)
      • 第三方插件市场(营销插件、支付插件)
      • 合作伙伴计划(供应商、物流商、支付商)
  2. 技术创新

    • Serverless架构

      • 函数计算(FaaS)替代部分微服务
      • 按需计费(降低运维成本50%)
      • 自动扩容(无需手动扩容)
      • 适用场景:短信通知、数据清洗、报表生成
    • Edge Computing

      • CDN边缘计算(静态资源、动态渲染)
      • 边缘缓存(用户就近访问,降低延迟)
      • 边缘函数(简单业务逻辑在边缘执行)
      • 预期收益:首屏加载时间降低60%
    • 区块链溯源

      • 高端商品防伪(奢侈品、珠宝)
      • 全链路追溯(生产、流通、销售)
      • 不可篡改(区块链存证)
      • 增强用户信任

改进路线图

gantt
    title 系统改进路线图
    dateFormat YYYY-MM
    section 短期(3个月)
    性能优化           :2026-05, 3M
    稳定性提升         :2026-05, 3M
    开发效率           :2026-05, 3M
    
    section 中期(6-12个月)
    智能化             :2026-08, 12M
    国际化             :2026-08, 12M
    数据驱动           :2026-08, 12M
    
    section 长期(1-3年)
    平台化             :2027-08, 24M
    技术创新           :2027-08, 24M

关键里程碑

时间里程碑成功标准
2026-08性能优化完成P99延迟 < 150ms,QPS提升30%
2026-11稳定性提升完成故障恢复时间 < 3分钟,可用性 > 99.99%
2027-02智能化上线推荐点击率+15%,动态定价毛利率+8%
2027-05国际化完成支持5种语言,10种币种,海外订单占比20%
2027-08数据驱动成熟A/B测试平台日活10万+,用户画像覆盖率100%
2028-08平台化初步完成开放API日调用100万+,接入第三方100+
2029-08技术创新落地Serverless占比30%,边缘计算覆盖80%流量

16.11 本章小结

本章通过一个中大型B2B2C电商平台的完整案例,展示了从业务分析到技术落地的全过程,是全书知识点的综合实践验证。本章不仅覆盖了架构方法论(第1-4章),还深入展示了**供给运营系统(第10章)C端核心交易流(第11-15章)**的完整实现,真正做到了"理论→实践→落地"的闭环。


核心要点回顾

1. 品类差异化设计是关键

不同品类的业务模型存在本质差异,这是架构设计的基础:

品类库存模型价格模型履约模式超卖容忍度
机票实时库存(供应商)动态定价异步出票零容忍
酒店日历库存日历定价异步确认零容忍
充值无限库存固定面额同步充值可补偿
优惠券券码池固定折扣即时发放可补偿

设计启示

  • ✅ 使用策略模式处理品类差异(避免 if-else 地狱)
  • ✅ 使用适配器模式统一供应商接口(降低耦合)
  • ✅ 模板方法定义统一流程(具体步骤由策略实现)
  • ❌ 避免"一刀切"架构(机票与充值差异巨大,不能用同一套逻辑)

2. 聚合层解决跨服务编排问题

API Gateway(职责单一)
   ↓ 鉴权、限流、路由
Aggregation Service(编排层)
   ↓ 并发调用、数据聚合、降级处理
Business Services(业务层)
   ↓ 单一职责、独立部署
Infrastructure(基础设施层)

为什么需要聚合层?

  • ✅ API Gateway保持职责单一(鉴权、限流、路由)
  • ✅ 复杂编排逻辑集中管理(搜索场景:ES → Product → Inventory → Marketing → Pricing)
  • ✅ 统一降级策略(Marketing故障降级为基础价)
  • ✅ 性能优化空间大(并发调用、批量查询、缓存聚合结果)

3. 架构决策记录(ADR)是宝贵资产

本章记录了13个关键ADR决策:

ADR编号决策主题核心价值
ADR-001计价中心数据输入方式聚合层传入 vs 计价层自己调用
ADR-002库存预占时机试算 vs 创单
ADR-003聚合服务 vs BFF按业务场景 vs 按端
ADR-004虚拟商品库存模型二维模型(ManagementType + UnitType)
ADR-005同步 vs 异步数据流核心路径同步,非核心异步
ADR-009创单时是否使用快照强制实时查询(安全优先)
ADR-010创单与支付的时序先创单后支付(防止超卖)
ADR-011前后端价格校验策略差异容忍 + 提示机制
ADR-012试算与创单价格计算统一引擎 + 差异化数据来源
ADR-013价格流转全局策略分阶段计算 + 逐步扩展维度

ADR的价值

  • ✅ 记录决策背景(新人快速了解"为什么这样设计")
  • ✅ 避免重复讨论(已解决的问题有文档可查)
  • ✅ 架构演进有据可查(回顾历史决策,持续优化)
  • ✅ 与代码一起版本管理(决策与实现同步演进)

4. 系统边界清晰至关重要

案例1:计价系统的边界重构

  • 问题:价格计算逻辑分散在订单、营销、商品三个域
  • 重构:新建计价上下文,提供统一试算接口
  • 收益:价格一致性得到保证,营销规则变更只需在营销域发布事件

案例2:库存预占的归属

  • 争议:库存预占应该放在订单域还是库存域?
  • 决策:放在库存域
  • 理由:库存域拥有库存数据所有权,预占是库存的一种状态,订单域只需调用库存域的 Reserve 接口

案例3:防腐层保护领域模型

// 供应商响应模型(外部)
type SupplierFlightResponse struct {
    Code    string
    Message string
    Data    struct {...}
}

// 平台库存模型(内部)
type StockResponse struct {
    Available bool
    Quantity  int
    Message   string
}

// 防腐层:翻译外部模型 → 内部模型
func (a *FlightSupplierACL) TranslateStock(supplierResp) *StockResponse {
    // 领域层不被供应商模型污染
}

5. 高可用需要多层防护

层级措施工具/技术
应用层服务多副本、自动扩容Kubernetes HPA
接口层熔断、降级、限流gobreaker、Feature Flag
缓存层多级缓存(本地+Redis+DB)BigCache + Redis
数据层主从复制、读写分离MySQL Replication
机房层多机房部署、灰度发布Multi-Region + Canary

稳定性三板斧

  • 熔断:供应商调用失败率>50%,熔断10秒
  • 降级:Marketing Service故障,降级为基础价
  • 限流:令牌桶算法,QPS=500

6. 供给运营是平台的核心能力(新增16.5.6)

三种核心场景

场景业务语义处理逻辑审核策略
商品上架新商品首次进入平台Create完整审核流程
供应商同步供应商数据变更Upsert差异化审核
运营编辑已上线商品维护Update差异化审核

设计要点

  • 幂等性保证:task_code唯一索引(上架)、sync_id唯一索引(同步)
  • 差异化审核:高风险变更(价格变化>50%、类目变更)必须审核
  • 批量操作:异步任务 + 进度追踪(100+ SKU批量编辑)
  • 状态机:DRAFT → PENDING → APPROVED → PUBLISHED
  • 与商品中心集成:审核通过后写入商品中心、初始化库存/价格

7. C端交易流贯穿整个业务链路(新增16.5.7)

五个阶段完整设计

搜索(Query理解+ES召回+Hydrate)
   ↓ 转化率 > 15%
详情页(多服务聚合+快照生成)
   ↓ 转化率 > 8%
购物车(未登录加购+登录合并+双写)
   ↓ 转化率 > 30%
结算页(价格试算+库存检查+优惠校验)
   ↓ 转化率 > 60%
下单支付(Saga编排+实时查询+价格校验)
   ↓ 转化率 > 85%

关键技术

  • Hydrate编排:并发调用4-5个服务(Product、Inventory、Pricing、Marketing)
  • 快照机制:详情页生成快照(5分钟TTL),结算页可选使用(性能优先)
  • 购物车合并:未登录Redis存储,登录后合并到用户购物车
  • Saga编排:下单时依次执行库存预占、优惠券锁定、价格计算、订单创建
  • 强制实时查询:创单时不使用任何快照(ADR-009,安全优先)

8. DDD战术设计落地实践(新增16.5.8)

Order聚合根设计

// 聚合根
type Order struct {
    orderID OrderID          // 值对象(聚合根ID)
    items   []*OrderItem     // 实体集合
    pricing *OrderPricing    // 值对象
    status  OrderStatus      // 值对象
    domainEvents []DomainEvent // 领域事件
}

// 值对象:OrderID(不可变)
// 值对象:OrderPricing(无ID,通过属性比较相等性)
// 实体:OrderItem(有ID,可变)

Repository + Outbox模式

  • Repository接口在领域层定义(不依赖基础设施)
  • 领域事件与业务在同一事务(Outbox表)
  • Outbox轮询器:定时扫描未发布事件,发布到Kafka

领域事件

// OrderStatusChangedEvent(订单状态变更)
// OrderItemAddedEvent(商品项添加)
// OrderCreatedEvent(订单创建)

9. 团队协作与技术治理同等重要

康威定律实践

订单团队(15人)→ 订单服务
商品团队(12人)→ 商品中心
库存团队(10人)→ 库存服务
...

契约测试加速并行开发

  • ✅ 上下游团队定义API契约(OpenAPI/Proto)
  • ✅ 消费者编写契约测试(Pact)
  • ✅ 提供者验证契约(契约测试通过后联调)
  • ✅ 契约变更影响提前发现(CI自动运行)

技术治理机制

  • ✅ ADR记录重大决策
  • ✅ 代码评审清单(架构、设计、代码、测试)
  • ✅ 技术债管理(优先级、负责人、工作量)
  • ✅ 定期架构Review(每季度)

实战价值

本章不是空洞的理论,而是200+人团队、日订单200万级的真实实践总结:

成功经验(值得借鉴):

  1. ADR制度:让架构演进有据可查,新人快速上手
  2. 品类差异化:策略模式让新品类接入成本降低80%
  3. 聚合编排:API Gateway职责单一,性能优化空间大
  4. 多级缓存:P99延迟从500ms降低到200ms
  5. 契约测试:团队并行开发,集成测试成本降低70%

踩过的坑(避坑指南):

  1. 过早引入Event Sourcing:团队理解不足,查询复杂,运维困难
  2. 供应商接口未做熔断:某供应商故障,整个订单服务不可用
  3. 分库分表过早:订单量100万就分库分表,运维复杂度激增
  4. 忽视数据一致性对账:库存预占后未释放,累积1个月后严重不准确
  5. 缓存穿透导致雪崩:恶意请求查询不存在的商品,数据库连接池耗尽

改进方向(持续演进):

  • 短期(3个月):性能优化、稳定性提升、开发效率
  • 中期(6-12个月):智能化、国际化、数据驱动
  • 长期(1-3年):平台化、技术创新

与其他章节的关系

本章是全书知识点的综合应用与实践验证:

前置章节在本章的应用
第1章(架构方法论)Clean Architecture分层、DDD战略设计(16.5.8)、CQRS读写分离
第2章(领域驱动设计)12个限界上下文划分、上下文映射、防腐层(16.6.4)
第3章(代码整洁)策略模式(品类策略)、适配器模式(供应商集成)、SOLID原则
第4章(质量保障)ADR(13个决策)、代码评审清单、测试策略
第7章(商品中心)SPU/SKU模型、类目属性、商品快照
第8章(库存系统)二维库存模型、预占机制、超时释放(16.5.2)
第9章(营销系统)营销规则引擎、优惠券锁定、最优解求解
第10章(供给运营)商品上架、供应商同步、运营编辑(16.5.6 新增)
第11章(计价系统)四层价格模型、试算接口、快照生成
第12章(搜索导购)Query→Recall→Rank→Hydrate链路(16.5.7 新增)
第13章(购物车结算)未登录加购、登录合并、Saga编排(16.5.7 新增)
第14章(订单系统)状态机、Saga模式、幂等性(16.5.7、16.5.8 新增)
第15章(支付系统)支付创建、回调处理、对账流程

第17章预告:将继续讲解系统演进与重构,本章的演进路径是铺垫。


给读者的建议

  1. 不要盲目照搬架构

    • 根据团队规模调整(10人团队不需要12个微服务)
    • 根据业务特点优化(B2C和B2B2C差异大)
    • 根据发展阶段选择(初创期先单体,成熟期再拆分)
  2. 架构是演进出来的

    • 先简单方案(单体应用、MySQL单表)
    • 再根据瓶颈优化(QPS瓶颈→缓存,数据量瓶颈→分库分表)
    • 避免过度设计(YAGNI原则:You Aren't Gonna Need It)
  3. ADR是宝贵财富

    • 记录决策过程(不只是结果)
    • 记录备选方案(为什么不选A而选B)
    • 记录权衡取舍(有什么优点和缺点)
    • 定期Review(每季度回顾,持续优化)
  4. 从错误中学习

    • 本章的"踩过的坑"是避坑指南
    • 建立错误知识库(每个错误都是学习机会)
    • 持续改进(错误 → 规则 → 自动化检查)
  5. 关注业务价值

    • 技术服务于业务(不是为了炫技)
    • 优先解决业务痛点(性能瓶颈、稳定性问题)
    • 量化技术收益(P99延迟降低、QPS提升、成本节省)

关键数据回顾

指标数值说明
团队规模200+人前台60、中台80、基础设施30、数据20、测试10
日订单量200万(正常)/ 1000万(大促)大促5倍流量
服务数量12个核心服务 + 3个聚合服务按业务能力拆分,单一职责
ADR数量13个记录重大架构决策
响应时间P99 < 200ms(正常)/ 500ms(大促)多级缓存优化
可用性99.95%(核心链路)多层防护
代码覆盖率> 80%单元测试 + 集成测试

导航返回目录 | 上一章 | 书籍主页 | 下一章