B2B2C电商平台系统架构设计(200人团队、日订单200万级)

B2B2C电商平台系统架构设计

项目背景:设计一个中大型B2B2C电商平台,主要连接外部供应商(机票、酒店、充值、电影票等虚拟数字商品),同时支持自营商品(优惠券、礼品卡)。平台规模:200+人团队,日订单200万,大促峰值1000万订单/天。


一、业务背景与架构目标

1.1 业务模式

核心业务

  • B2B2C聚合模式:连接50+外部供应商,聚合机票、酒店、账单充值、电影票等虚拟商品
  • 自营模式:自营优惠券(e-voucher)、线下券、礼品卡等
  • 无物流场景:全部为虚拟数字商品,无需物流配送

关键特征

  • 供应商接口高度碎片化(实时查询 + 定时同步 + 推送混合)
  • 核心品类(机票/酒店)零超卖容忍
  • 长尾品类(充值/礼品卡)可事后补偿

1.2 品类业务模型差异

不同品类的业务模型存在显著差异,直接影响架构设计决策:

(1)机票(Flight)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
业务特点:
• 库存模型:实时库存(供应商侧),强依赖供应商实时查询
• 价格模型:动态定价,实时波动(可能秒级变化)
• SKU复杂度:极高(航司+航班号+舱位+日期+...组合)
• 库存单位:座位数量(不可超卖)
• 扣减时机:下单即扣(预占)→ 支付确认 → 出票
• 履约流程:下单 → 支付 → 出票(调用GDS/供应商API)→ 发送电子票

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

技术要点:
• Redis缓存TTL:5分钟(库存)、10分钟(价格)
• 供应商调用超时:800ms(实时查询)
• 熔断阈值:错误率>50%,熔断10秒

(2)酒店(Hotel)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
业务特点:
• 库存模型:房间数量(按日期维度管理)
• 价格模型:日历房价(每个日期不同价格)
• SKU复杂度:高(酒店ID+房型+日期范围+早餐+...)
• 库存单位:房间数/间夜数
• 扣减时机:下单预占 → 支付确认 → 供应商确认
• 履约流程:下单 → 支付 → 提交供应商 → 确认单 → 入住凭证

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

技术要点:
• 价格存储:时间序列数据库或宽表(date维度)
• 库存粒度:SKU_ID + Date(复合键)
• 缓存策略:热门酒店30分钟,长尾酒店1小时
• 供应商确认:异步轮询(每30秒查询一次状态)

(3)充值(Top-up / Recharge)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
业务特点:
• 库存模型:无限库存(供应商侧无限制)
• 价格模型:固定面额(10元、50元、100元)
• SKU复杂度:低(运营商+面额)
• 库存单位:无限
• 扣减时机:支付后
• 履约流程:下单 → 支付 → 调用供应商API → 充值成功/失败

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

技术要点:
• 库存管理:不需要预占,直接下单
• 价格缓存:1小时(价格稳定)
• 供应商调用:同步调用,3秒超时
• 重试策略:3次重试,指数退避(1s, 2s, 4s)

(4)账单缴费(Bill Payment)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
业务特点:
• 库存模型:无库存概念(代收代付)
• 价格模型:查询实时账单金额
• SKU复杂度:低(账单类型+账号)
• 库存单位:无
• 扣减时机:支付后
• 履约流程:查询账单 → 下单 → 支付 → 缴费成功 → 回执

架构影响:
✓ 需要"查账单"接口(调用供应商)
✓ 金额动态(每次查询不同)
✓ 幂等性要求极高(避免重复缴费)
✓ 对账要求严格(需与供应商流水对账)

技术要点:
• 查账单缓存:5分钟(避免频繁查询)
• 幂等Token:前端生成,5分钟有效
• 对账频率:每小时一次
• 供应商调用:同步,5秒超时

(5)电影票(Movie Ticket)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
业务特点:
• 库存模型:实时库存(座位级别)
• 价格模型:动态定价(场次+座位+时段)
• SKU复杂度:极高(影院+影片+场次+座位号)
• 库存单位:座位(精确到排号座号)
• 扣减时机:选座即锁定(15分钟)→ 支付确认
• 履约流程:选座 → 锁座(15min)→ 支付 → 出票码

架构影响:
✓ 座位锁定机制(15分钟倒计时)
✓ 实时库存(座位图需秒级更新)
✓ 超卖零容忍(用户体验极差)
✓ 高并发场景(热门场次抢票)
✓ 座位状态复杂(可售、锁定、已售、维修)

技术要点:
• 库存粒度:SKU_ID + SeatNo(精确到座位)
• 锁座机制:Redis SETNX + 15分钟TTL
• 座位图缓存:实时推送(WebSocket)
• 热门场次限流:令牌桶算法,QPS=500

(6)Deal/线下优惠券(Voucher)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
业务特点:
• 库存模型:固定库存(券码池)
• 价格模型:固定折扣价
• SKU复杂度:中(商户+门店+商品+...)
• 库存单位:券码(一券一码)
• 扣减时机:支付后
• 履约流程:下单 → 支付 → 发券码 → 到店核销

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

技术要点:
• 券码存储:Redis Set(未使用券码池)
• 发券逻辑:SPOP(原子弹出一个券码)
• 有效期管理:ZSet按过期时间排序
• 核销接口:幂等性(同一券码只能核销一次)

(7)礼品卡(Gift Card)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
业务特点:
• 库存模型:无限库存(虚拟卡)
• 价格模型:固定面额 or 自定义金额
• SKU复杂度:低(面额)
• 库存单位:卡号
• 扣减时机:支付后
• 履约流程:下单 → 支付 → 生成卡号+卡密 → 发送

架构影响:
✓ 卡号生成算法(保证唯一性)
✓ 余额管理(卡内余额扣减)
✓ 多次使用(支持部分消费)
✓ 转赠功能(卡可以转给他人)
✓ 对账复杂(发卡、消费、退款流水)

技术要点:
• 卡号生成:雪花算法 + 校验位
• 余额存储:Redis Hash(cardNo -> balance)
• 消费记录:MySQL + ES(双写)
• 并发控制:乐观锁(版本号)

1.3 品类差异对架构的影响

维度 机票/酒店(核心品类) 充值/账单(长尾品类) Deal/礼品卡(自营)
库存管理 实时同步,强一致 无限库存,无需管理 券码池,异步补充
价格策略 动态定价,实时变化 固定面额,稳定 固定折扣,活动价
超卖容忍度 零容忍(P0故障) 可补偿(P2故障) 低容忍(P1故障)
供应商依赖 强依赖,需实时调用 弱依赖,异步批量 无依赖(自营)
缓存TTL 5-10分钟 30-60分钟 1-24小时
熔断阈值 50%错误率 70%错误率 不需要熔断
对账频率 每5分钟 每小时 每天
履约复杂度 高(多状态机) 低(同步返回) 中(券码+核销)

架构设计启示

  1. 不能一刀切:不同品类需要不同的库存策略、缓存策略、对账策略
  2. 策略模式:通过策略模式实现品类差异化逻辑(见后续”库存服务设计”)
  3. 优先级分级:核心品类(机票/酒店)优先保障,长尾品类可降级
  4. 供应商分级:P0供应商(机票)熔断阈值更严格,P2供应商(充值)更宽松
  5. 监控分级:核心品类错误率>0.1%告警,长尾品类错误率>1%告警

1.4 规模指标

指标 日常 大促峰值 备注
日订单量 200万 1000万 5倍峰值
日活用户 500万 2000万 4倍峰值
QPS峰值 5万 25万 API Gateway总QPS
商品SKU 500万 - 包含供应商商品
团队规模 200人 - 研发+测试+运维

1.5 架构目标(优先级排序)

  1. 高可用性(P0):核心服务SLA ≥ 99.95%
  2. 数据一致性(P0):订单/库存/资金数据强一致
  3. 供应商容错(P1):单个供应商故障不影响平台
  4. 高性能(P1):P99延迟 < 1秒
  5. 弹性扩展(P2):支持5-10倍弹性扩容

二、整体架构设计

2.1 七层架构模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
┌────────────────────────────────────────────────────────────────┐
│ L1: 用户层(User Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Web端:React/Vue │ │
│ │ • 移动端:iOS/Android原生 + RN/Flutter │ │
│ │ • 小程序:微信/支付宝小程序 │ │
│ └──────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ L2: 网关层(Gateway Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • API Gateway (APISIX):统一入口、限流、鉴权 │ │
│ │ • BFF (Backend For Frontend):端特定逻辑聚合 │ │
│ └──────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ L3: 业务服务层(Business Service Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 核心域: │ │
│ │ • Order Service (订单) │ │
│ │ • Payment Service (支付) │ │
│ │ • Checkout Service (结算) │ │
│ │ • Inventory Service (库存) │ │
│ │ │ │
│ │ 支撑域: │ │
│ │ • Product Center (商品中心) │ │
│ │ • Pricing Service (计价引擎) │ │
│ │ • Marketing Service (营销) │ │
│ │ • Search Service (搜索) │ │
│ │ • Aggregation Service (聚合服务,编排层) │ │
│ │ • Cart Service (购物车) │ │
│ │ • User Service (用户) │ │
│ │ • Listing Service (商品上架) │ │
│ │ │ │
│ │ 供应商域: │ │
│ │ • Supplier Gateway (供应商网关) │ │
│ │ • Supplier Sync (供应商同步) │ │
│ └──────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ L4: 供应商网关层(Supplier Gateway Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • 供应商适配器(Plugin架构,每个供应商独立插件) │ │
│ │ • 协议转换(HTTP/SOAP/gRPC统一适配) │ │
│ │ • 熔断降级(Hystrix) │ │
│ │ • 限流重试(智能退避) │ │
│ └──────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ L5: 中间件层(Middleware Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • MySQL (分库分表、主从复制) │ │
│ │ • Redis Cluster (三级缓存:本地+Redis+DB) │ │
│ │ • Kafka (事件总线、3副本、24h保留) │ │
│ │ • Elasticsearch (商品搜索、5分片×2副本) │ │
│ └──────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ L6: 基础设施层(Infrastructure Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Service Mesh (Istio/Linkerd) │ │
│ │ • 配置中心 (Nacos/Apollo) │ │
│ │ • 注册中心 (Consul/Etcd) │ │
│ │ • 链路追踪 (Jaeger/Skywalking) │ │
│ │ • 监控告警 (Prometheus + Grafana) │ │
│ │ • 日志收集 (ELK Stack) │ │
│ └──────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ L7: 部署层(Deployment Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Kubernetes (容器编排、HPA弹性伸缩) │ │
│ │ • 同城双活部署 (IDC-A + IDC-B) │ │
│ │ • DNS智能解析 (GeoDNS) │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

2.2 微服务拆分(19个核心服务)

服务名称 职责 QPS(常态) QPS(大促) 副本数(常/大促)
api-gateway 统一入口、鉴权、限流 50000 250000 4 / 12
order-service 订单管理、状态机 3000 15000 10 / 30
payment-service 支付集成、回调处理 3000 15000 10 / 30
checkout-service 结算、试算、确认下单 2000 10000 8 / 24
inventory-service 库存管理、预占、扣减 5000 25000 8 / 24
product-center 商品主数据、SPU/SKU 8000 40000 6 / 18
pricing-service 四层计价(基础价+促销+费用+券) 4000 20000 8 / 24
marketing-service 优惠券、活动、规则引擎 2000 10000 6 / 18
search-service ES查询、索引管理 6000 30000 6 / 18
aggregation-service 数据聚合、编排多服务调用(搜索、详情等) 6000 30000 6 / 18
cart-service 购物车增删改查 4000 20000 6 / 18
supplier-gateway 供应商调用适配、熔断、重试 10000 50000 10 / 30
supplier-sync 供应商数据同步(定时任务) - - 4 / 4
listing-service 商品上架、审核、状态机 500 2000 4 / 12
user-service 用户信息、会员等级 2000 10000 4 / 12
notification-service 消息通知(短信/邮件/推送) 2000 10000 4 / 12
analytics-service 数据上报、埋点 5000 25000 4 / 12
admin-service 运营后台管理 200 200 2 / 2
task-scheduler 定时任务调度 - - 2 / 2

三、核心服务设计

3.1 聚合服务(Aggregation Service)

服务定位

职责:通用聚合服务,编排多服务调用,按依赖关系顺序聚合数据。

核心场景

1. 商品列表场景(Item/SPU维度)
查询方式 查询维度 ES查询字段 返回粒度 典型场景
关键字搜索 keyword title, description, tags Item列表(SPU) 用户输入”无线耳机”
分类浏览 category_id category_id Item列表(SPU) 点击”数码配件”分类
筛选查询 brand, price_range, attrs brand, price, attributes Item列表(SPU) 筛选”苹果品牌+500-1000元”
推荐列表 user_id, item_id 推荐算法 Item列表(SPU) “猜你喜欢”、”相关推荐”

编排流程:ES查询(获取item_ids) → Product Center(商品基础信息+base_price) → Inventory(库存状态) → Marketing(营销活动) → Pricing(计算最终价格)

数据特点

  • ✅ 批量查询:一次返回20-50个商品
  • ✅ 性能优先:支持降级(营销/库存可降级)
  • ✅ 缓存友好:列表结果可缓存10分钟
2. 商品详情场景(SKU维度)
查询方式 查询维度 返回粒度 典型场景
商品详情页 item_id Item信息 + 所有SKU详情 用户点击商品进入详情页
SKU详情 sku_id 单个SKU详细信息 用户选择规格后查询库存/价格

编排流程:Product Center(商品+SKU详情) → Inventory(SKU库存) → Marketing(SKU级营销) → Pricing(SKU价格) → Review(评价) → Recommendation(相关推荐)

数据特点

  • ✅ 细粒度查询:返回SKU级别的库存、价格、属性
  • ✅ 完整性优先:不支持降级(库存/价格必须准确)
  • ✅ 实时性强:缓存TTL较短(1-5分钟)
3. 其他查询场景(扩展)
  • 订单详情聚合:订单 → 商品 → 物流 → 售后
  • 用户中心聚合:用户 → 订单 → 优惠券 → 积分
  • 购物车聚合:购物车 → 商品 → 库存 → 价格 → 营销

与其他服务的区别

  • vs Search Service:Search Service只负责ES查询,不包含业务逻辑
  • vs BFF:不区分端(Web/App共用),专注于数据聚合编排
  • vs Checkout Service:Checkout是交易编排,Aggregation是查询编排
  • vs Product Center:Product Center是数据源,Aggregation是编排层

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
aggregation-service/
├── cmd/
│ └── main.go
├── internal/
│ ├── application/
│ │ ├── dto/
│ │ │ ├── search_request.go # 搜索/列表请求
│ │ │ ├── search_response.go # 搜索/列表响应
│ │ │ ├── detail_request.go # 详情请求
│ │ │ └── detail_response.go # 详情响应
│ │ └── service/
│ │ ├── search_orchestrator.go # 列表场景编排器(Item/SPU维度)
│ │ ├── detail_orchestrator.go # 详情场景编排器(SKU维度)
│ │ └── cart_orchestrator.go # 购物车场景编排器(扩展)
│ ├── infrastructure/
│ │ ├── rpc/
│ │ │ ├── search_client.go # Search Service客户端
│ │ │ ├── product_client.go # Product Center客户端
│ │ │ ├── inventory_client.go # Inventory客户端
│ │ │ ├── marketing_client.go # Marketing客户端
│ │ │ ├── pricing_client.go # Pricing客户端
│ │ │ ├── review_client.go # Review Service客户端
│ │ │ └── recommendation_client.go # Recommendation客户端
│ │ ├── cache/
│ │ │ └── redis_cache.go # Redis缓存
│ │ ├── circuitbreaker/
│ │ │ └── breaker.go # 熔断器
│ │ └── event/
│ │ └── kafka_publisher.go # 用户行为事件
│ └── interfaces/
│ ├── http/
│ │ ├── search_handler.go # 搜索/列表接口
│ │ └── detail_handler.go # 详情接口
│ └── grpc/
│ ├── search_handler.go
│ └── detail_handler.go
├── config/
│ └── config.yaml
└── go.mod

核心编排逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// SearchOrchestrator 搜索编排器
type SearchOrchestrator struct {
searchClient rpc.SearchClient
productClient rpc.ProductClient
inventoryClient rpc.InventoryClient
marketingClient rpc.MarketingClient
pricingClient rpc.PricingClient
cache cache.Cache
}

// Search 搜索商品(编排多服务调用)
func (o *SearchOrchestrator) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
// Step 1: 缓存检查
cacheKey := o.buildCacheKey(req)
if cached, err := o.cache.Get(ctx, cacheKey); err == nil {
return cached, nil
}

// Step 2: 调用Search Service查询ES,获取sku_ids
searchResult, err := o.searchClient.SearchES(ctx, &SearchESRequest{
Keyword: req.Keyword,
Page: req.Page,
Size: req.Size,
})
if err != nil {
return nil, fmt.Errorf("search ES failed: %w", err)
}

skuIDs := searchResult.SkuIDs // [1001, 1002, ..., 1020]

// Step 3: 并发调用Product + Inventory(无依赖关系)
var (
products []*Product
stocks map[int64]*StockInfo
wg sync.WaitGroup
errChan = make(chan error, 2)
)

wg.Add(2)

// 并发调用1:Product Center
go func() {
defer wg.Done()
var err error
products, err = o.productClient.BatchGetProducts(ctx, skuIDs)
if err != nil {
errChan <- fmt.Errorf("get products failed: %w", err)
}
}()

// 并发调用2:Inventory Service
go func() {
defer wg.Done()
var err error
stocks, err = o.inventoryClient.BatchCheckStock(ctx, skuIDs)
if err != nil {
errChan <- fmt.Errorf("check stock failed: %w", err)
}
}()

wg.Wait()
close(errChan)

// 检查错误(Product是核心依赖,必须成功)
for err := range errChan {
if err != nil && strings.Contains(err.Error(), "get products") {
return nil, err // Product失败直接返回
}
// Inventory失败降级处理(隐藏库存)
}

// 构建商品基础价格map(来自Product Center)
basePriceMap := make(map[int64]float64)
for _, p := range products {
basePriceMap[p.SkuID] = p.BasePrice
}

// Step 4: 调用Marketing Service获取营销信息
promos, err := o.marketingClient.BatchGetPromotions(ctx, &PromotionRequest{
SkuIDs: skuIDs,
UserID: req.UserID, // 个性化营销
})
if err != nil {
// Marketing失败降级:无促销信息
promos = make(map[int64]*PromotionInfo)
}

// Step 5: 调用Pricing Service计算最终价格
// 输入:base_price + 营销信息
priceItems := make([]*PriceCalculateItem, 0, len(skuIDs))
for _, skuID := range skuIDs {
priceItems = append(priceItems, &PriceCalculateItem{
SkuID: skuID,
BasePrice: basePriceMap[skuID], // 来自Product Center
PromoInfo: promos[skuID], // 来自Marketing Service
Quantity: 1,
})
}

prices, err := o.pricingClient.BatchCalculatePrice(ctx, priceItems)
if err != nil {
// Pricing失败降级:只展示base_price
prices = o.buildFallbackPrices(basePriceMap)
}

// Step 6: 数据聚合
items := o.aggregateResults(products, stocks, promos, prices)

// Step 7: 写入缓存(异步)
go func() {
o.cache.Set(context.Background(), cacheKey, items, 10*time.Minute)
}()

// Step 8: 发布搜索事件(异步)
go func() {
o.publishSearchEvent(context.Background(), req, searchResult)
}()

return &SearchResponse{
Total: searchResult.Total,
Items: items,
Filters: searchResult.Filters,
}, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// DetailOrchestrator 详情编排器(SKU维度)
type DetailOrchestrator struct {
productClient rpc.ProductClient
inventoryClient rpc.InventoryClient
marketingClient rpc.MarketingClient
pricingClient rpc.PricingClient
reviewClient rpc.ReviewClient
recommendationClient rpc.RecommendationClient
cache cache.Cache
}

// GetItemDetail 获取商品详情(Item + 所有SKU信息)
func (o *DetailOrchestrator) GetItemDetail(ctx context.Context, req *DetailRequest) (*DetailResponse, error) {
// Step 1: 缓存检查(详情页缓存TTL较短:1-5分钟)
cacheKey := fmt.Sprintf("item_detail:%d:user:%d", req.ItemID, req.UserID)
if cached, err := o.cache.Get(ctx, cacheKey); err == nil {
return cached, nil
}

// Step 2: 获取商品基础信息(Item + 所有SKU)
itemDetail, err := o.productClient.GetItemDetail(ctx, req.ItemID)
if err != nil {
return nil, fmt.Errorf("get item detail failed: %w", err)
}

skuIDs := itemDetail.SkuIDs // [1001, 1002, 1003] - 所有规格的SKU

// Step 3: 并发调用多个服务(提升性能)
var (
stocks map[int64]*StockInfo
promos map[int64]*PromoInfo
reviews *ReviewSummary
recommend []*RecommendItem
wg sync.WaitGroup
mu sync.Mutex
errs []error
)

wg.Add(4)

// 并发调用1:Inventory Service(SKU库存)
go func() {
defer wg.Done()
var err error
stocks, err = o.inventoryClient.BatchCheckStock(ctx, skuIDs)
if err != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("inventory failed: %w", err))
mu.Unlock()
}
}()

// 并发调用2:Marketing Service(SKU营销)
go func() {
defer wg.Done()
var err error
promos, err = o.marketingClient.BatchGetPromotions(ctx, skuIDs, req.UserID)
if err != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("marketing failed: %w", err))
mu.Unlock()
}
}()

// 并发调用3:Review Service(评价汇总)
go func() {
defer wg.Done()
var err error
reviews, err = o.reviewClient.GetReviewSummary(ctx, req.ItemID)
if err != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("review failed: %w", err))
mu.Unlock()
}
}()

// 并发调用4:Recommendation Service(相关推荐)
go func() {
defer wg.Done()
var err error
recommend, err = o.recommendationClient.GetRelatedItems(ctx, req.ItemID, 10)
if err != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("recommendation failed: %w", err))
mu.Unlock()
}
}()

wg.Wait()

// 详情页关键数据失败不降级,直接返回错误
if len(errs) > 0 {
for _, err := range errs {
if strings.Contains(err.Error(), "inventory failed") {
return nil, err // 库存是详情页核心数据,不可降级
}
}
}

// Step 4: 调用Pricing Service计算每个SKU的价格
prices, err := o.pricingClient.BatchCalculatePrice(ctx, &PriceRequest{
Items: buildPriceItems(itemDetail, promos),
})
if err != nil {
return nil, fmt.Errorf("calculate price failed: %w", err)
}

// Step 5: 数据聚合(包含营销信息处理)
skuDetails := make([]*SkuDetail, 0, len(skuIDs))
for _, skuID := range skuIDs {
skuDetails = append(skuDetails, &SkuDetail{
SkuID: skuID,
Attributes: itemDetail.SkuAttributes[skuID], // 颜色、尺码等
Stock: stocks[skuID], // 库存状态
Price: prices[skuID], // 最终价格
Promotion: promos[skuID], // 营销活动
})
}

// 构建营销信息(吸引用户多买)
promotions := buildPromotionDetails(promos, itemDetail)
// promotions包含:
// - active_promotions: 多买优惠、组合优惠、品类促销
// - coupons: 可用优惠券列表
// - saving_tips: "再买1件,可省XXX元"

resp := &DetailResponse{
Item: itemDetail.Item,
SkuDetails: skuDetails,
Promotions: promotions, // 营销信息(促进多买)
ReviewSummary: reviews,
Recommendation: recommend,
}

// Step 6: 写入缓存(TTL: 1-5分钟)
_ = o.cache.Set(ctx, cacheKey, resp, 5*time.Minute)

// Step 7: 发布用户行为事件(异步)
go o.publishViewEvent(ctx, req.ItemID, req.UserID)

return resp, nil
}

列表 vs 详情场景对比

维度 搜索/列表场景 商品详情场景
查询粒度 Item/SPU(商品级别) SKU(规格级别)
返回数量 20-50个商品 1个商品 + N个SKU
ES查询 需要(关键字/分类/筛选) 不需要(直接通过item_id查询)
库存查询 可降级(隐藏库存) 不可降级(核心数据)
营销信息 简单(单品折扣) 详细(多买优惠、组合优惠、省钱提示)
营销目的 吸引点击 吸引多买、提升客单价
价格计算 可降级(base_price) 不可降级(必须准确)
缓存TTL 10分钟(列表变化慢) 1-5分钟(价格/库存变化快)
降级策略 支持多级降级 关键数据不降级
用户行为 浏览、点击 详细查看、加购

调用链路可视化

场景1:搜索/列表场景(SearchOrchestrator)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Aggregation Service编排流程:

Stage 1: ES查询(独立)
↓ item_ids / sku_ids
Stage 2: 并发调用(无依赖)
├─ Product Center (base_price, info)
└─ Inventory Service (stock_info)

Stage 3: Marketing Service(依赖sku_ids)
↓ promo_info
Stage 4: Pricing Service(依赖base_price + promo_info)
↓ final_price
Stage 5: 聚合返回

总耗时:50 + 50 + 70 + 100 + 20 = 290ms
缓存命中:5ms(80%场景)
降级场景:50 + 50 = 100ms(只返回商品基础信息)

场景2:商品详情场景(DetailOrchestrator)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Aggregation Service编排流程:

Stage 1: Product Center查询(获取Item + 所有SKU)
↓ item_detail + sku_ids
Stage 2: 并发调用(无依赖,4个服务)
├─ Inventory Service (SKU库存)
├─ Marketing Service (SKU营销)
├─ Review Service (评价汇总)
└─ Recommendation Service (相关推荐)

Stage 3: Pricing Service(依赖base_price + promo_info)
↓ final_price (每个SKU)
Stage 4: 聚合返回

总耗时:50 + max(30, 70, 40, 50) + 100 = 220ms
缓存命中:5ms(50%场景)
注:详情页关键数据(库存/价格)不降级

降级策略矩阵

搜索/列表场景(SearchOrchestrator)

服务 是否核心依赖 失败处理 对用户的影响
Search Service 返回错误 搜索不可用
Product Center 返回错误 搜索不可用
Inventory Service 降级(隐藏库存) 不显示库存状态
Marketing Service 降级(无促销) 只展示基础价
Pricing Service 降级(base_price) 展示基础价,无促销价

商品详情场景(DetailOrchestrator)

服务 是否核心依赖 失败处理 对用户的影响
Product Center 返回错误 详情页不可用
Inventory Service 返回错误 无法下单(库存是关键数据)
Pricing Service 返回错误 无法下单(价格是关键数据)
Marketing Service 返回错误 价格计算依赖营销规则
Review Service 降级(隐藏评价) 无评价展示
Recommendation Service 降级(无推荐) 无相关推荐

3.2 库存服务(Inventory Service)

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
inventory-service/
├── cmd/
│ └── main.go
├── internal/
│ ├── domain/
│ │ ├── model/
│ │ │ ├── inventory.go # 库存聚合根
│ │ │ ├── stock_unit.go # 库存单元(券码/数量/时间)
│ │ │ └── reservation.go # 预占记录
│ │ ├── value_object/
│ │ │ ├── management_type.go # 管理类型(自营/供应商/无限)
│ │ │ ├── unit_type.go # 单位类型
│ │ │ └── deduct_timing.go # 扣减时机
│ │ ├── repository/
│ │ │ └── inventory_repo.go # 仓储接口
│ │ └── service/
│ │ ├── stock_calculator.go # 库存计算领域服务
│ │ └── reservation_manager.go # 预占管理
│ ├── application/
│ │ ├── dto/
│ │ │ ├── inventory_dto.go
│ │ │ └── reserve_dto.go
│ │ └── service/
│ │ ├── inventory_app_service.go # 库存应用服务
│ │ ├── reserve_app_service.go # 预占应用服务
│ │ ├── sync_app_service.go # 同步应用服务
│ │ └── reconcile_app_service.go # 对账应用服务
│ ├── infrastructure/
│ │ ├── persistence/
│ │ │ ├── mysql/
│ │ │ │ ├── inventory_repo_impl.go
│ │ │ │ └── migrations/
│ │ │ └── redis/
│ │ │ ├── inventory_cache.go
│ │ │ └── lua/
│ │ │ ├── reserve_stock.lua # 原子预占脚本
│ │ │ └── release_stock.lua # 原子释放脚本
│ │ ├── strategy/
│ │ │ ├── self_managed_strategy.go # 自营库存策略
│ │ │ ├── supplier_strategy.go # 供应商库存策略
│ │ │ └── unlimited_strategy.go # 无限库存策略
│ │ ├── supplier/
│ │ │ ├── sync_adapter.go # 供应商同步适配器
│ │ │ └── realtime_checker.go # 实时库存查询
│ │ ├── event/
│ │ │ └── kafka_publisher.go # Kafka事件发布
│ │ ├── rpc/
│ │ │ ├── product_client.go
│ │ │ └── supplier_client.go
│ │ └── job/
│ │ ├── cleanup_expired_reserves.go # 清理过期预占
│ │ └── reconcile_job.go # 库存对账任务
│ └── interfaces/
│ ├── grpc/
│ │ ├── inventory_handler.go
│ │ └── proto/
│ └── http/
│ └── inventory_controller.go
├── config/
│ └── config.yaml
└── go.mod

核心领域模型

库存聚合根(Inventory)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
type Inventory struct {
ID int64
SKUID int64
TotalStock int64 // 总库存
AvailableStock int64 // 可用库存
ReservedStock int64 // 预占库存
SoldStock int64 // 已售库存

// 库存策略
ManagementType ManagementType // 1:自营 2:供应商 3:无限
UnitType UnitType // 1:券码 2:数量 3:时间
DeductTiming DeductTiming // 1:下单扣 2:支付扣

// 供应商相关
SupplierID *int64
SyncStrategy string // realtime/scheduled/push
LastSyncAt *time.Time

// 预占记录(聚合内存)
reservations []*Reservation

// 版本控制
Version int
UpdatedAt time.Time
}

// Reserve 预占库存(领域方法)
func (inv *Inventory) Reserve(quantity int64, orderID string, userID int64) (*Reservation, error) {
if inv.AvailableStock < quantity {
return nil, ErrInsufficientStock
}

reservation := &Reservation{
ID: uuid.New().String(),
SKUID: inv.SKUID,
OrderID: orderID,
UserID: userID,
Quantity: quantity,
Status: ReservationStatusPending,
ExpiresAt: time.Now().Add(15 * time.Minute),
CreatedAt: time.Now(),
}

inv.AvailableStock -= quantity
inv.ReservedStock += quantity
inv.reservations = append(inv.reservations, reservation)

// 发布领域事件
inv.publishEvent(&StockReservedEvent{
SKUID: inv.SKUID,
OrderID: orderID,
Quantity: quantity,
})

return reservation, nil
}

Redis原子操作(Lua脚本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- reserve_stock.lua (原子库存预占)
local sku_key = KEYS[1] -- inventory:sku:12345
local reserve_key = KEYS[2] -- reserve:uuid-xxx
local expire_zset_key = KEYS[3] -- reserve:expiry

local quantity = tonumber(ARGV[1])
local reserve_data = ARGV[2] -- JSON序列化的预占数据
local expires_at = tonumber(ARGV[3]) -- Unix时间戳
local ttl = tonumber(ARGV[4]) -- 15分钟

-- 检查库存
local available = tonumber(redis.call('HGET', sku_key, 'available'))
if not available or available < quantity then
return {err = 'insufficient_stock'}
end

-- 扣减可用库存、增加预占库存
redis.call('HINCRBY', sku_key, 'available', -quantity)
redis.call('HINCRBY', sku_key, 'reserved', quantity)

-- 写入预占记录
redis.call('SET', reserve_key, reserve_data, 'EX', ttl)

-- 添加到过期索引(用于定时清理)
redis.call('ZADD', expire_zset_key, expires_at, reserve_key)

return {ok = 'success', available = available - quantity}

3.3 商品上架服务(Listing Service)

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
listing-service/
├── cmd/
│ └── main.go
├── internal/
│ ├── domain/
│ │ ├── model/
│ │ │ ├── listing_task.go # 上架任务聚合根
│ │ │ ├── audit_record.go # 审核记录
│ │ │ └── publish_record.go # 发布记录
│ │ ├── value_object/
│ │ │ ├── task_status.go # 任务状态枚举
│ │ │ └── audit_result.go # 审核结果
│ │ ├── repository/
│ │ │ └── listing_repo.go
│ │ └── service/
│ │ ├── state_machine.go # 状态机领域服务
│ │ ├── audit_router.go # 审核路由(按品类)
│ │ └── validator.go # 业务规则校验
│ ├── application/
│ │ ├── dto/
│ │ │ ├── listing_dto.go
│ │ │ └── audit_dto.go
│ │ ├── service/
│ │ │ ├── listing_app_service.go # 上架应用服务
│ │ │ ├── audit_app_service.go # 审核应用服务
│ │ │ ├── publish_app_service.go # 发布应用服务
│ │ │ └── batch_import_service.go # 批量导入
│ │ └── saga/
│ │ └── publish_saga.go # 发布Saga编排
│ ├── infrastructure/
│ │ ├── persistence/
│ │ │ ├── mysql/
│ │ │ └── redis/
│ │ ├── state_machine/
│ │ │ ├── config/
│ │ │ │ └── transitions.yaml # 状态转换配置
│ │ │ ├── guard/ # 状态转换守卫
│ │ │ └── handler/ # 状态处理器
│ │ ├── audit/
│ │ │ └── strategy/
│ │ │ ├── manual_audit.go # 人工审核
│ │ │ ├── auto_audit.go # 自动审核
│ │ │ └── risk_audit.go # 风控审核
│ │ ├── datasource/
│ │ │ ├── supplier_sync.go # 供应商同步
│ │ │ ├── excel_import.go # Excel导入
│ │ │ └── api_import.go # API导入
│ │ ├── rpc/
│ │ │ ├── product_client.go
│ │ │ ├── inventory_client.go
│ │ │ ├── pricing_client.go
│ │ │ └── search_client.go
│ │ ├── event/
│ │ │ └── kafka_publisher.go
│ │ └── job/
│ │ ├── auto_publish_job.go # 定时自动发布
│ │ └── expire_check_job.go # 过期检查
│ └── interfaces/
│ ├── grpc/
│ └── http/
├── config/
│ ├── config.yaml
│ └── state_machine.yaml
└── go.mod

状态机设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// StateMachine 状态机接口
type StateMachine interface {
Transition(ctx context.Context, task *ListingTask, event Event) error
CanTransition(currentStatus TaskStatus, event Event) bool
}

// StateMachineImpl 状态机实现
type StateMachineImpl struct {
transitions map[TaskStatus]map[Event]TaskStatus // 状态转换表
guards map[Event]Guard // 转换守卫
handlers map[TaskStatus]Handler // 状态处理器
}

// Transition 状态转换
func (sm *StateMachineImpl) Transition(ctx context.Context, task *ListingTask, event Event) error {
// 1. 检查转换合法性
if !sm.CanTransition(task.Status, event) {
return fmt.Errorf("invalid transition: %s -> %s", task.Status, event)
}

// 2. 执行守卫检查
if guard, ok := sm.guards[event]; ok {
if err := guard.Check(ctx, task); err != nil {
return fmt.Errorf("guard check failed: %w", err)
}
}

// 3. 获取目标状态
targetStatus := sm.transitions[task.Status][event]

// 4. 执行状态处理器
if handler, ok := sm.handlers[targetStatus]; ok {
if err := handler.Handle(ctx, task); err != nil {
return fmt.Errorf("handler failed: %w", err)
}
}

// 5. 更新状态
oldStatus := task.Status
task.Status = targetStatus
task.UpdatedAt = time.Now()

// 6. 发布领域事件
task.PublishEvent(&StatusChangedEvent{
TaskID: task.ID,
OldStatus: oldStatus,
NewStatus: targetStatus,
Event: event,
})

return nil
}

Saga编排(发布流程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// PublishSaga 发布Saga编排器
type PublishSaga struct {
productClient rpc.ProductClient
inventoryClient rpc.InventoryClient
pricingClient rpc.PricingClient
searchClient rpc.SearchClient
}

// Execute 执行发布Saga
func (s *PublishSaga) Execute(ctx context.Context, task *ListingTask) error {
// 定义步骤
steps := []SagaStep{
&CreateProductStep{client: s.productClient},
&InitInventoryStep{client: s.inventoryClient},
&SetupPricingStep{client: s.pricingClient},
&IndexSearchStep{client: s.searchClient},
}

executedSteps := make([]SagaStep, 0)

// 顺序执行步骤
for _, step := range steps {
if err := step.Execute(ctx, task); err != nil {
// 触发补偿
s.compensate(ctx, task, executedSteps)
return fmt.Errorf("saga step %s failed: %w", step.Name(), err)
}
executedSteps = append(executedSteps, step)
}

return nil
}

// compensate 补偿逻辑
func (s *PublishSaga) compensate(ctx context.Context, task *ListingTask, executedSteps []SagaStep) {
// 逆序执行补偿
for i := len(executedSteps) - 1; i >= 0; i-- {
step := executedSteps[i]
if err := step.Compensate(ctx, task); err != nil {
// 记录补偿失败日志,进入人工处理队列
log.Error("compensation failed", "step", step.Name(), "error", err)
}
}
}

3.4 供应商网关(Supplier Gateway)

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
supplier-gateway-service/
├── cmd/
│ └── main.go
├── internal/
│ ├── domain/
│ │ ├── model/
│ │ │ ├── supplier.go
│ │ │ └── supplier_config.go
│ │ └── repository/
│ │ └── supplier_repo.go
│ ├── application/
│ │ ├── dto/
│ │ └── service/
│ │ ├── gateway_service.go
│ │ ├── health_check_service.go
│ │ └── metrics_service.go
│ ├── infrastructure/
│ │ ├── adapter/
│ │ │ ├── interface.go # 供应商适配器接口
│ │ │ ├── base_adapter.go # 基础适配器(模板方法)
│ │ │ ├── flight/
│ │ │ │ ├── supplier_a_adapter.go # 机票供应商A
│ │ │ │ └── supplier_b_adapter.go
│ │ │ ├── hotel/
│ │ │ │ ├── supplier_c_adapter.go
│ │ │ │ └── supplier_d_adapter.go
│ │ │ └── protocol/
│ │ │ ├── http_converter.go # HTTP协议转换
│ │ │ ├── soap_converter.go # SOAP协议转换
│ │ │ └── grpc_converter.go # gRPC协议转换
│ │ ├── circuit_breaker/
│ │ │ └── hystrix_wrapper.go # Hystrix熔断器
│ │ ├── rate_limiter/
│ │ │ ├── token_bucket.go # 令牌桶算法
│ │ │ ├── sliding_window.go # 滑动窗口算法
│ │ │ └── redis_limiter.go # 分布式限流(Redis)
│ │ ├── retry/
│ │ │ ├── exponential_backoff.go # 指数退避
│ │ │ └── fixed_backoff.go # 固定间隔
│ │ ├── router/
│ │ │ ├── load_balancer.go # 负载均衡(多供应商)
│ │ │ └── failover.go # 故障切换
│ │ ├── monitor/
│ │ │ ├── metrics_collector.go # 指标采集
│ │ │ ├── health_checker.go # 健康检查
│ │ │ └── alerter.go # 告警
│ │ └── cache/
│ │ ├── config_cache.go # 配置缓存
│ │ └── response_cache.go # 响应缓存(幂等)
│ └── interfaces/
│ └── grpc/
├── config/
│ ├── config.yaml
│ └── suppliers/ # 供应商配置(外部化)
│ ├── flight/
│ │ ├── supplier_a.yaml
│ │ └── supplier_b.yaml
│ └── hotel/
│ ├── supplier_c.yaml
│ └── supplier_d.yaml
└── go.mod

供应商适配器接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// SupplierAdapter 供应商适配器接口
type SupplierAdapter interface {
// 查询商品库存
QueryStock(ctx context.Context, req *StockQueryRequest) (*StockQueryResponse, error)

// 创建订单
CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, error)

// 查询订单状态
QueryOrder(ctx context.Context, orderID string) (*OrderStatusResponse, error)

// 取消订单
CancelOrder(ctx context.Context, orderID string) error

// 健康检查
HealthCheck(ctx context.Context) error
}

// BaseAdapter 基础适配器(模板方法模式)
type BaseAdapter struct {
config *SupplierConfig
httpClient *http.Client
circuitBreaker *hystrix.CircuitBreaker
rateLimiter RateLimiter
retryPolicy RetryPolicy
}

// Execute 执行模板方法(封装通用逻辑)
func (a *BaseAdapter) Execute(ctx context.Context, operation string, fn func() (interface{}, error)) (interface{}, error) {
// 1. 限流检查
if a.rateLimiter != nil && a.config.RateLimit.Enabled {
if err := a.rateLimiter.Allow(ctx, a.config.SupplierID); err != nil {
return nil, fmt.Errorf("rate limit exceeded: %w", err)
}
}

// 2. 熔断执行
if a.config.CircuitBreaker.Enabled {
return a.circuitBreaker.Execute(func() (interface{}, error) {
return a.executeWithRetry(ctx, operation, fn)
})
}

return a.executeWithRetry(ctx, operation, fn)
}

// executeWithRetry 带重试的执行
func (a *BaseAdapter) executeWithRetry(ctx context.Context, operation string, fn func() (interface{}, error)) (interface{}, error) {
var lastErr error

for attempt := 1; attempt <= a.config.Retry.MaxAttempts; attempt++ {
// 记录指标
startTime := time.Now()

result, err := fn()

duration := time.Since(startTime)
a.recordMetrics(operation, duration, err)

if err == nil {
return result, nil
}

lastErr = err

// 判断是否可重试
if !a.isRetryable(err) {
break
}

// 退避等待
if attempt < a.config.Retry.MaxAttempts {
backoff := a.retryPolicy.NextBackoff(attempt)
time.Sleep(backoff)
}
}

return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
}

供应商配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# config/suppliers/flight/supplier_a.yaml
supplier_id: "flight_supplier_a"
supplier_type: "flight"
supplier_name: "航空供应商A"
priority: "P0" # P0:核心 P1:重要 P2:一般

# 协议配置
protocol: "HTTP"
base_url: "https://api.supplier-a.com"
timeout: 800ms
auth:
type: "api_key"
api_key: "${SUPPLIER_A_API_KEY}" # 环境变量

# 熔断配置
circuit_breaker:
enabled: true
error_threshold: 60.0 # 错误率阈值60%
min_requests: 20 # 最小请求数
timeout: 1000 # 熔断超时1秒
sleep_window: 5000 # 熔断后等待5秒进入半开状态

# 限流配置
rate_limit:
enabled: true
qps: 500 # 每秒500次请求
burst_size: 600 # 突发容量600
time_window: 1 # 时间窗口1秒

# 重试配置
retry:
enabled: true
max_attempts: 3
backoff_policy: "exponential" # exponential/fixed
initial_delay: 100 # 初始延迟100ms
max_delay: 1000 # 最大延迟1秒
retryable_errors:
- "timeout"
- "connection_refused"
- "503"

# 降级配置
fallback:
enabled: false
fallback_data: null

# 监控配置
monitor:
enabled: true
alert_threshold:
error_rate: 10.0 # 错误率>10%告警
latency_p99: 2000 # P99延迟>2秒告警

四、数据流设计

4.1 同步 vs 异步

分类原则

场景 调用方式 典型用例
同步RPC 用户等待、需要立即返回结果 结算试算、库存查询、下单、支付
异步事件 非阻塞、最终一致性 订单状态变更通知、搜索索引更新、数据分析

4.2 用户搜索商品(导购场景)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
场景:用户在首页搜索"无线耳机"

┌─────────────────────────────────────────────────────────────┐
│ 搜索商品时序图(同步调用) │
├─────────────────────────────────────────────────────────────┤
│ │
│ [APP/Web] │
│ ↓ GET /search?keyword=无线耳机&page=1&size=20 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ [API Gateway] ││
│ │ • 鉴权:验证JWT Token(可选,支持游客搜索) ││
│ │ • 限流:IP限流100次/分钟,防爬虫 ││
│ │ • 路由:转发到 Aggregation Service ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 鉴权通过,转发请求 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ [Aggregation Service] - 聚合服务(编排层) ││
│ │ 职责:编排多个微服务调用,聚合数据返回 ││
│ │ ││
│ │ Step 1: 缓存检查 ││
│ │ key = "search:无线耳机:page1:size20" ││
│ │ ├─ 缓存命中 → 直接返回(5ms)✓ ││
│ │ └─ 缓存未命中 → 继续查询 ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 缓存未命中 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 2: 调用Search Service查询ES ││
│ │ ┌─────────────────────────────────────────────────┐ ││
│ │ │ [Search Service] - 搜索服务 │ ││
│ │ │ ↓ 调用 Elasticsearch │ ││
│ │ │ ┌─────────────────────────────────────────┐ │ ││
│ │ │ │ [Elasticsearch] │ │ ││
│ │ │ │ • 全文搜索:"无线耳机"(中文分词) │ │ ││
│ │ │ │ • 过滤条件:status=online │ │ ││
│ │ │ │ • 排序规则:综合排序(销量+价格+评分) │ │ ││
│ │ │ │ • 分页:from=0, size=20 │ │ ││
│ │ │ │ • 高亮:标题、描述中的关键词高亮 │ │ ││
│ │ │ │ • 聚合:品牌、价格区间、分类(筛选项) │ │ ││
│ │ │ │ 查询耗时:30-50ms │ │ ││
│ │ │ └─────────────────────────────────────────┘ │ ││
│ │ │ ↓ 返回 │ ││
│ │ │ { │ ││
│ │ │ "sku_ids": [1001, 1002, ..., 1020], │ ││
│ │ │ "total": 1230, │ ││
│ │ │ "filters": {...} // 筛选项聚合 │ ││
│ │ │ } │ ││
│ │ │ 50ms │ ││
│ │ └─────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 获得 sku_ids: [1001, 1002, ..., 1020] │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 3: 并发调用基础数据服务(无依赖关系) ││
│ │ ┌────────────────────┐ ┌────────────────────┐ ││
│ │ │ [Product Center] │ │ [Inventory Service]│ ││
│ │ │ RPC: BatchGet │ │ RPC: BatchCheck │ ││
│ │ │ Products │ │ Stock │ ││
│ │ │ (sku_ids) │ │ (sku_ids) │ ││
│ │ │ ↓ │ │ ↓ │ ││
│ │ │ 返回: │ │ 返回: │ ││
│ │ │ • title │ │ • available_stock │ ││
│ │ │ • images │ │ • stock_status │ ││
│ │ │ • brand │ │ (in_stock/ │ ││
│ │ │ • category │ │ out_of_stock) │ ││
│ │ │ • base_price ✓ │ │ • sold_count │ ││
│ │ │ • attributes │ │ │ ││
│ │ │ 50ms │ │ 30ms │ ││
│ │ └────────────────────┘ └────────────────────┘ ││
│ │ ││
│ │ 并发调用,总耗时:max(50, 30) = 50ms ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 获得商品基础信息 + 基础价格 + 库存状态 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 4: 调用Marketing Service获取营销信息 ││
│ │ ┌──────────────────────────────────────────────────┐ ││
│ │ │ [Marketing Service] │ ││
│ │ │ RPC: BatchGetPromotions(sku_ids, user_id) │ ││
│ │ │ ↓ │ ││
│ │ │ 返回每个SKU的营销活动: │ ││
│ │ │ • promo_id: 活动ID │ ││
│ │ │ • promo_type: 折扣/满减/限时购 │ ││
│ │ │ • discount_rate: 0.9(九折) │ ││
│ │ │ • discount_amount: 400(满2000减400) │ ││
│ │ │ • available_coupons: 可用优惠券列表 │ ││
│ │ │ 70ms │ ││
│ │ └──────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 获得营销信息 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 5: 调用Pricing Service计算最终价格 ││
│ │ (依赖Step 3的base_price + Step 4的营销信息) ││
│ │ ┌──────────────────────────────────────────────────┐ ││
│ │ │ [Pricing Service] │ ││
│ │ │ RPC: BatchCalculatePrice(items) │ ││
│ │ │ 输入: │ ││
│ │ │ [ │ ││
│ │ │ { │ ││
│ │ │ "sku_id": 1001, │ ││
│ │ │ "base_price": 2399.00, │ ││
│ │ │ "promo_id": "PROMO_001", │ ││
│ │ │ "discount_rate": 0.9, │ ││
│ │ │ "quantity": 1 │ ││
│ │ │ }, │ ││
│ │ │ ... │ ││
│ │ │ ] │ ││
│ │ │ ↓ │ ││
│ │ │ 内部计算流程: │ ││
│ │ │ 1. 应用促销折扣 │ ││
│ │ │ promo_price = base_price × discount_rate │ ││
│ │ │ = 2399 × 0.9 = 2159.1 │ ││
│ │ │ 2. 查询Fee配置(服务费、税费) │ ││
│ │ │ fee = 0(部分商品免服务费) │ ││
│ │ │ 3. 计算最终价格 │ ││
│ │ │ final_price = promo_price + fee │ ││
│ │ │ = 2159.1 + 0 = 2159.1 │ ││
│ │ │ ↓ │ ││
│ │ │ 返回每个SKU的价格信息: │ ││
│ │ │ • original_price: 2399.00(原价) │ ││
│ │ │ • promo_price: 2159.00(促销价) │ ││
│ │ │ • discount_amount: 240.00(优惠金额) │ ││
│ │ │ • fee: 0 │ ││
│ │ │ • final_price: 2159.00(最终价格) │ ││
│ │ │ 100ms │ ││
│ │ └──────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 获得最终价格 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 6: 数据聚合与处理 ││
│ │ • 合并:商品信息 + 价格 + 库存 + 营销 ││
│ │ • 无货商品置底或隐藏 ││
│ │ • 图片CDN地址拼接 ││
│ │ • 敏感信息过滤(成本价、供应商ID等) ││
│ │ • 个性化排序(已登录用户基于历史行为调整) ││
│ │ 处理耗时:20ms ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 聚合完成 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 7: 写入缓存(异步,不阻塞返回) ││
│ │ ├─ Redis SET "search:无线耳机:page1:size20" result ││
│ │ ├─ TTL: 10分钟(热门搜索词) ││
│ │ └─ 后台任务:记录搜索日志到Kafka(用于搜索分析) ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ │
│ [APP/Web] ← 返回搜索结果 │
│ { │
│ "total": 1230, │
│ "items": [ │
│ { │
│ "sku_id": 1001, │
│ "title": "Sony无线<em>耳机</em> WH-1000XM5", │
│ "brand": "Sony", │
│ "image": "https://cdn.example.com/1001.jpg", │
│ "price": { │
│ "original": 2399.00, // Product Center │
│ "promo": 2159.00, // Pricing Service │
│ "discount": 240.00, // 优惠金额 │
│ "promo_tag": "限时9折" // Marketing Service │
│ }, │
│ "stock": { │
│ "status": "in_stock", // Inventory Service │
│ "available": 450, │
│ "message": "现货充足" │
│ }, │
│ "sales": 12580 // Search Service ES │
│ }, │
│ ... │
│ ], │
│ "filters": { // 筛选项聚合结果(来自ES) │
│ "brands": ["Sony", "Bose", "Apple", ...], │
│ "price_ranges": ["0-500", "500-1000", ...] │
│ } │
│ } │
│ │
│ 总耗时(实时聚合): │
│ ES查询(50ms) │
│ + 并发调用Product+Inventory(50ms) │
│ + Marketing(70ms) │
│ + Pricing(100ms) │
│ + 数据聚合(20ms) │
│ = 50 + 50 + 70 + 100 + 20 = 290ms │
│ │
│ P95延迟:< 350ms(实时聚合完整流程) │
│ P95延迟:< 50ms(Redis缓存命中,80%+场景) │
└─────────────────────────────────────────────────────────────┘

异步流程(不阻塞用户):
┌─────────────────────────────────────────────────────────────┐
│ 1. 用户搜索行为追踪 │
│ [Kafka Topic: search.query] │
│ { │
│ "user_id": 67890, │
│ "keyword": "无线耳机", │
│ "result_count": 1230, │
│ "clicked_sku_ids": [], │
│ "timestamp": 1776138000 │
│ } │
│ ↓ │
│ 订阅者: │
│ ├─→ [Analytics Service] 监听:搜索热词统计、转化率分析 │
│ ├─→ [Recommendation Service] 监听:更新用户画像 │
│ └─→ [Search Service] 监听:优化搜索排序算法(A/B Test) │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 2. ES索引价格更新(保证搜索结果价格准确性) │
│ [Kafka Topic: price.updated] │
│ 发布者:Pricing Service(定时任务或营销活动触发) │
│ { │
│ "sku_ids": [1001, 1002, ...], │
│ "prices": [ │
│ { │
│ "sku_id": 1001, │
│ "base_price": 2399.00, │
│ "promo_price": 1999.00, │
│ "discount_amount": 400.00, │
│ "discount_reason": "满2000减400", │
│ "valid_until": 1776224400 │
│ }, │
│ ... │
│ ], │
│ "timestamp": 1776138000 │
│ } │
│ ↓ │
│ 订阅者: │
│ └─→ [Search Service] 监听:批量更新ES索引中的价格字段 │
│ UPDATE product_index SET │
│ base_price = ?, │
│ promo_price = ?, │
│ discount_amount = ? │
│ WHERE sku_id IN (...) │
│ │
│ 更新频率: │
│ • 定时任务:每5分钟全量更新(增量更新有变化的商品) │
│ • 营销活动生效:实时推送(如限时折扣开始/结束) │
└─────────────────────────────────────────────────────────────┘

关键设计要点

  1. Aggregation Service(聚合服务)

    • 职责:编排多个微服务的调用顺序,聚合数据
    • 与BFF的区别:专注于搜索场景,不区分端(Web/App共用)
    • 为什么需要:
      • 解耦业务逻辑(Search Service只负责ES查询)
      • 统一编排(避免客户端多次RPC调用)
      • 便于扩展(新增数据源只需修改聚合服务)
    • 部署:独立微服务,6副本(QPS 6000)
  2. 分阶段调用策略(关键)

    • 严格按依赖关系顺序调用,不能完全并发
    • 阶段1:Search Service查询ES → 获得sku_ids(50ms)
    • 阶段2:并发调用Product Center + Inventory Service(50ms)
      • 无依赖关系,可并发
      • Product返回商品信息 + base_price(关键)
    • 阶段3:调用Marketing Service获取营销信息(70ms)
      • 需要sku_ids作为输入
    • 阶段4:调用Pricing Service计算最终价格(100ms)
      • 依赖:base_price(来自Product) + 营销信息(来自Marketing)
      • 内部逻辑:应用折扣 + 计算Fee
    • 总耗时:50 + 50 + 70 + 100 + 20(聚合) = 290ms
  3. 数据依赖关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    ┌──────────────────────────────────────────────┐
    │ 服务调用依赖关系图 │
    ├──────────────────────────────────────────────┤
    │ │
    │ Search Service (ES查询) │
    │ ↓ 提供 sku_ids │
    │ ┌────────────────┐ ┌────────────────┐ │
    │ │ Product Center │ │ Inventory Svc │ │
    │ │ (并发) │ │ (并发) │ │
    │ └────────────────┘ └────────────────┘ │
    │ ↓ 提供 base_price │
    │ ┌────────────────┐ │
    │ │ Marketing Svc │ │
    │ └────────────────┘ │
    │ ↓ 提供 discount_info │
    │ ┌────────────────┐ │
    │ │ Pricing Service│ ← 依赖 base_price + │
    │ │ │ discount_info │
    │ └────────────────┘ │
    │ ↓ 返回 final_price │
    │ 聚合返回 │
    └──────────────────────────────────────────────┘
  4. 多级缓存

    • L1缓存(Redis):完整搜索结果10分钟缓存,命中率80%+
    • L2缓存(ES本地缓存):ES节点本地缓存
    • 缓存Key设计:search:{keyword}:{page}:{size}:{user_id?}
    • 个性化场景:已登录用户加user_id,未登录用户共享缓存
  5. 降级策略

    • Marketing Service异常 → Pricing使用base_price,无折扣
    • Pricing Service异常 → 只展示base_price,标记”价格加载中”
    • Inventory Service异常 → 隐藏库存信息
    • Product Service异常 → 整个搜索失败(核心依赖)
  6. 性能优化

    • 批量接口:所有RPC都使用BatchXXX批量接口
    • 超时控制:每个RPC 200ms超时,避免雪崩
    • 熔断保护:错误率>50%自动熔断
    • 限流保护:Aggregation Service QPS 6000
  7. 性能指标

    • P50延迟:< 50ms(Redis缓存命中,80%场景)
    • P95延迟:< 350ms(实时聚合完整流程)
    • P99延迟:< 500ms
    • QPS峰值:6000(Aggregation Service需6副本)

4.3 查询商品详情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
场景:用户从搜索结果点击进入商品详情页

┌─────────────────────────────────────────────────────────────┐
│ 商品详情时序图(同步 + 缓存) │
├─────────────────────────────────────────────────────────────┤
│ │
│ [APP/Web] │
│ ↓ GET /products/12345/detail │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ [API Gateway] ││
│ │ • 鉴权:可选(游客可查看) ││
│ │ • 限流:用户限流30次/分钟(防刷) ││
│ │ • CDN:静态资源(图片、视频)CDN加速 ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 路由到 Product Center │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ [Product Center] - 商品中心 ││
│ │ Step 1: 三级缓存查询 ││
│ │ ┌─────────────────────────────────────────────────┐ ││
│ │ │ L1: 本地缓存(Ristretto) │ ││
│ │ │ key = "product:12345" │ ││
│ │ │ ├─ 命中 → 返回(<1ms)✓ 80%命中率 │ ││
│ │ │ └─ 未命中 → 查L2 │ ││
│ │ │ ↓ │ ││
│ │ │ L2: Redis(分布式缓存) │ ││
│ │ │ key = "product:sku:12345" │ ││
│ │ │ ├─ 命中 → 返回(<5ms)✓ 95%命中率 │ ││
│ │ │ └─ 未命中 → 查L3 │ ││
│ │ │ ↓ │ ││
│ │ │ L3: MySQL(权威数据) │ ││
│ │ │ SELECT * FROM product WHERE sku_id=12345 │ ││
│ │ │ └─ 返回(10-30ms) │ ││
│ │ └─────────────────────────────────────────────────┘ ││
│ │ ↓ 获得商品基础信息 ││
│ │ { ││
│ │ "sku_id": 12345, ││
│ │ "spu_id": 5678, ││
│ │ "title": "Sony WH-1000XM5 无线降噪耳机", ││
│ │ "brand": "Sony", ││
│ │ "category_id": 101, ││
│ │ "images": ["img1.jpg", "img2.jpg", ...], ││
│ │ "attributes": { ││
│ │ "color": "黑色", ││
│ │ "connectivity": "蓝牙5.2" ││
│ │ }, ││
│ │ "description": "...", ││
│ │ "status": "online" ││
│ │ } ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 并发查询其他维度数据(5个服务并发调用) │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 2: 并发调用多个微服务(扇出模式) ││
│ │ ││
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ││
│ │ │ [Pricing] │ │ [Inventory]│ │ [Marketing]│ ││
│ │ │ RPC: │ │ RPC: │ │ RPC: │ ││
│ │ │ Calculate │ │ GetStock │ │ GetProduct│ ││
│ │ │ Price │ │ (sku_id) │ │ Promotions│ ││
│ │ │ (sku_id) │ │ │ │ (sku_id, │ ││
│ │ │ ↓ │ │ ↓ │ │ user_id) │ ││
│ │ │ 返回: │ │ 返回: │ │ ↓ │ ││
│ │ │ base_price│ │ total: 500│ │ 返回: │ ││
│ │ │ 2399.00 │ │ available │ │ • 可用券 │ ││
│ │ │ promo: │ │ 450 │ │ • 单品折扣│ ││
│ │ │ 1999.00 │ │ reserved │ │ • 跨商品 │ ││
│ │ │ 100ms │ │ 50 │ │ 促销 │ ││
│ │ │ │ │ 30ms │ │ • 组合优惠│ ││
│ │ │ │ │ │ │ 80ms │ ││
│ │ └────────────┘ └────────────┘ └────────────┘ ││
│ │ ││
│ │ ┌────────────┐ ┌────────────┐ ││
│ │ │ [Review] │ │ [Recommend]│ ││
│ │ │ RPC: │ │ RPC: │ ││
│ │ │ GetReviews│ │ GetRelated│ ││
│ │ │ (sku_id) │ │ Products │ ││
│ │ │ ↓ │ │ (sku_id) │ ││
│ │ │ 返回: │ │ ↓ │ ││
│ │ │ 评分4.8 │ │ 推荐商品 │ ││
│ │ │ 评论列表 │ │ [sku_id │ ││
│ │ │ (top 10) │ │ list] │ ││
│ │ │ 60ms │ │ 120ms │ ││
│ │ └────────────┘ └────────────┘ ││
│ │ ││
│ │ 并发调用,总耗时:max(100,30,80,60,120) = 120ms ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 数据聚合 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 3: 数据聚合与个性化处理 ││
│ │ • 合并所有维度数据 ││
│ │ • 个性化推荐(已登录用户) ││
│ │ • 库存状态判断: ││
│ │ - available >= 10 → "现货充足" ││
│ │ - available < 10 → "仅剩X件" ││
│ │ - available = 0 → "暂时缺货,到货通知" ││
│ │ • 价格展示策略: ││
│ │ - 有促销 → 显示划线价 + 促销价 ││
│ │ - 有券 → 显示"券后价XXX元" ││
│ │ • 营销信息处理(吸引多买): ││
│ │ - 按优先级排序促销活动(多买优惠 > 组合优惠) ││
│ │ - 计算省钱提示:"再买1件,可省XXX元" ││
│ │ - 生成推荐购买数量(基于最优惠方案) ││
│ │ - 标记高价值促销(highlight: true) ││
│ │ • 敏感信息过滤(供应商信息、成本价等) ││
│ │ 处理耗时:20ms ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 聚合完成 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 4: 缓存回写 + 异步事件 ││
│ │ • 写入Redis(完整商品详情) ││
│ │ key = "product:detail:12345" ││
│ │ TTL = 30分钟 ││
│ │ • 写入本地缓存(热点商品) ││
│ │ • 发布浏览事件到Kafka(用户行为分析) ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ │
│ [APP/Web] ← 返回商品详情 │
│ { │
│ "sku_id": 12345, │
│ "title": "Sony WH-1000XM5 无线降噪耳机", │
│ "images": ["https://cdn.example.com/img1.jpg", ...],│
│ "price": { │
│ "original": 2399.00, │
│ "current": 1999.00, │
│ "coupon_available": true, │
│ "coupon_after": 1899.00 │
│ }, │
│ "stock": { │
│ "status": "in_stock", │
│ "message": "仅剩12件" │
│ }, │
│ "promotions": { // 营销信息(吸引多买)│
│ "active_promotions": [ // 当前生效的促销 │
│ { │
│ "id": "PROMO_001", │
│ "type": "multi_buy", // 多买优惠 │
│ "title": "买2件享9折", │
│ "description": "再买1件,立享9折优惠", │
│ "conditions": { │
│ "min_quantity": 2, │
│ "discount_rate": 0.9 │
│ }, │
│ "highlight": true, // 前端高亮显示 │
│ "expires_at": "2026-04-20 23:59:59" │
│ }, │
│ { │
│ "id": "PROMO_002", │
│ "type": "bundle", // 组合优惠 │
│ "title": "搭配充电器立减50元", │
│ "description": "购买耳机+充电器组合,减50元", │
│ "bundle_products": [ │
│ { │
│ "sku_id": 1005, │
│ "title": "Sony快充充电器", │
│ "price": 99.00, │
│ "discount": 50.00 │
│ } │
│ ] │
│ }, │
│ { │
│ "id": "PROMO_003", │
│ "type": "category_discount", // 品类折扣 │
│ "title": "配件类满3件享8折", │
│ "description": "音频配件买满3件,享受8折优惠", │
│ "conditions": { │
│ "category": "音频配件", │
│ "min_quantity": 3, │
│ "discount_rate": 0.8 │
│ } │
│ } │
│ ], │
│ "coupons": [ // 可用优惠券 │
│ { │
│ "code": "SAVE100", │
│ "title": "满2000减100", │
│ "threshold": 2000.00, │
│ "discount": 100.00, │
│ "expires_at": "2026-04-30" │
│ } │
│ ], │
│ "saving_tips": { // 省钱提示 │
│ "message": "再买1件,可享9折优惠,共省480元", │
│ "recommended_quantity": 2, │
│ "total_savings": 480.00 │
│ } │
│ }, │
│ "reviews": { │
│ "rating": 4.8, │
│ "count": 12580, │
│ "top_reviews": [...] │
│ }, │
│ "related_products": [...], // 相关推荐 │
│ "attributes": {...} // 商品属性 │
│ } │
│ │
│ 总耗时:缓存查询(5ms) + 并发RPC(120ms) + 聚合(20ms) │
│ = 145ms(P95 < 200ms,P99 < 300ms) │
└─────────────────────────────────────────────────────────────┘

异步流程(用户行为追踪):
┌─────────────────────────────────────────────────────────────┐
│ [Kafka Topic: user.behavior] │
│ 消息内容: │
│ { │
│ "user_id": 67890, │
│ "event_type": "view_product", │
│ "sku_id": 12345, │
│ "from_source": "search_result", // 来源 │
│ "timestamp": 1776138000 │
│ } │
│ ↓ │
│ 订阅者: │
│ ├─→ [Recommendation Service] 监听:更新用户兴趣标签 │
│ ├─→ [Analytics Service] 监听:漏斗分析(浏览→加购→下单) │
│ ├─→ [Marketing Service] 监听:触发再营销(浏览未购买) │
│ └─→ [Product Center] 监听:热度统计(更新商品热度排序) │
└─────────────────────────────────────────────────────────────┘

关键设计要点

  1. 三级缓存策略

    • L1本地缓存:5分钟TTL,热点商品命中率80%+
    • L2 Redis缓存:30分钟TTL,整体命中率95%+
    • L3 MySQL:权威数据源
  2. 并发调用优化

    • 5个微服务并发调用(扇出模式)
    • 使用超时控制(每个RPC 200ms超时)
    • 部分服务失败不影响主流程(降级)
  3. 营销信息展示(促进多买)

    • 多买优惠:展示”买2件享9折”,刺激用户增加购买数量
    • 组合优惠:推荐搭配商品(如耳机+充电器),提升客单价
    • 品类促销:展示”配件类满3件享8折”,引导用户购买同品类商品
    • 省钱提示:明确告知”再买1件,可省480元”,量化优惠金额
    • 高亮显示:重要促销信息前端高亮展示,提升转化率
    • 实时计算:根据用户已选商品,动态计算最优优惠方案
  4. 降级策略

    • Pricing异常 → 显示”价格加载中”或使用缓存价格
    • Inventory异常 → 隐藏库存信息或显示”请联系客服”
    • Marketing异常 → 隐藏营销模块(不影响购买)
    • Review异常 → 隐藏评论模块
    • Recommend异常 → 隐藏推荐商品或使用默认推荐
  5. 性能指标

    • P50延迟:< 50ms(本地缓存命中)
    • P95延迟:< 200ms(Redis缓存命中)
    • P99延迟:< 300ms
    • QPS峰值:8000(Product Center需6副本)
  6. 热点商品保护

    • 本地缓存前置(避免Redis热key)
    • 限流保护(单SKU QPS限制)
    • 降级开关(大促时关闭非核心功能如推荐)
  7. 营销数据来源

    • Marketing Service统一管理所有促销规则
    • 支持A/B测试(不同用户展示不同促销)
    • 促销活动实时生效(无需重启服务)
    • 缓存TTL短(5分钟),确保促销信息及时更新

4.4 加购与试算(无购物车模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
场景:用户选择多个商品(不同SKU),需要实时计算总价
适用于:快速结账场景(如电影票、充值卡等),无需持久化购物车

┌─────────────────────────────────────────────────────────────┐
│ 加购与试算时序图(同步调用) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户操作流程: │
│ 1. 在商品详情页选择 SKU + 数量(前端临时存储) │
│ 2. 可添加多个商品(前端维护临时列表) │
│ 3. 点击"结算"按钮,触发试算接口 │
│ │
│ [APP/Web] - 前端临时存储 │
│ selectedItems = [ │
│ { sku_id: 1001, quantity: 2, category: "耳机" }, │
│ { sku_id: 1005, quantity: 1, category: "充电器" }, │
│ { sku_id: 2003, quantity: 3, category: "数据线" } │
│ ] │
│ ↓ POST /checkout/calculate │
│ { │
│ "user_id": 67890, │
│ "items": [ │
│ { "sku_id": 1001, "quantity": 2 }, │
│ { "sku_id": 1005, "quantity": 1 }, │
│ { "sku_id": 2003, "quantity": 3 } │
│ ], │
│ "coupon_codes": ["SAVE50"] // 用户选择的优惠券 │
│ } │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ [API Gateway] ││
│ │ • 鉴权:必须登录(user_id验证) ││
│ │ • 限流:用户限流10次/分钟(防止恶意试算) ││
│ │ • 参数校验:items数量≤20(防止超大订单) ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 转发到 Checkout Service │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ [Checkout Service] - 结算服务(编排层) ││
│ │ 职责:编排多服务调用,计算订单总价 ││
│ │ ││
│ │ Step 1: 参数预处理与去重 ││
│ │ • 合并相同SKU(quantity累加) ││
│ │ • 去除无效SKU(quantity≤0) ││
│ │ • 构建sku_ids列表:[1001, 1005, 2003] ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 2: 并发调用基础数据服务(2个服务) ││
│ │ ││
│ │ ┌────────────────────┐ ┌────────────────────┐ ││
│ │ │ [Product Center] │ │ [Inventory Service]│ ││
│ │ │ RPC: BatchGet │ │ RPC: BatchCheck │ ││
│ │ │ Products │ │ Stock │ ││
│ │ │ (sku_ids) │ │ (sku_ids) │ ││
│ │ │ ↓ │ │ ↓ │ ││
│ │ │ 返回: │ │ 返回: │ ││
│ │ │ • base_price │ │ • available_stock │ ││
│ │ │ • title │ │ • stock_status │ ││
│ │ │ • category │ │ │ ││
│ │ │ 50ms │ │ 30ms │ ││
│ │ └────────────────────┘ └────────────────────┘ ││
│ │ ││
│ │ 并发调用,总耗时:max(50, 30) = 50ms ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 获得商品基础信息 + 基础价格 + 库存状态 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 3: 库存校验(关键步骤,决定是否可下单) ││
│ │ 遍历每个SKU,检查库存: ││
│ │ • available_stock >= quantity → 可下单 ││
│ │ • available_stock < quantity → 返回错误 ││
│ │ 错误信息:"商品[XXX]库存不足,仅剩N件" ││
│ │ • stock_status = "out_of_stock" → 返回错误 ││
│ │ 错误信息:"商品[XXX]已售罄" ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 库存校验通过 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 4: 调用Marketing Service获取营销活动 ││
│ │ ┌──────────────────────────────────────────────────┐ ││
│ │ │ [Marketing Service] │ ││
│ │ │ RPC: CalculatePromotions(items, user_id) │ ││
│ │ │ ↓ │ ││
│ │ │ 输入:商品列表 + 用户ID │ ││
│ │ │ • 商品级别促销: │ ││
│ │ │ - SKU 1001: 单件折扣9折 │ ││
│ │ │ - SKU 1005: 限时购特价 │ ││
│ │ │ • 跨商品促销(关键): │ ││
│ │ │ - 满300减50(全场) │ ││
│ │ │ - 买3件打8折(同品类) │ ││
│ │ │ - 组合优惠:耳机+充电器减20元 │ ││
│ │ │ • 用户优惠券: │ ││
│ │ │ - 券码: SAVE50(满500减50) │ ││
│ │ │ ↓ │ ││
│ │ │ 返回营销活动列表(按优先级排序): │ ││
│ │ │ [ │ ││
│ │ │ { │ ││
│ │ │ "promo_id": "P001", │ ││
│ │ │ "type": "sku_discount", // 商品级别折扣 │ ││
│ │ │ "sku_id": 1001, │ ││
│ │ │ "discount_rate": 0.9 │ ││
│ │ │ }, │ ││
│ │ │ { │ ││
│ │ │ "promo_id": "P002", │ ││
│ │ │ "type": "order_reduce", // 订单级别满减 │ ││
│ │ │ "threshold": 300, │ ││
│ │ │ "reduce": 50 │ ││
│ │ │ }, │ ││
│ │ │ { │ ││
│ │ │ "promo_id": "P003", │ ││
│ │ │ "type": "category_discount", // 品类折扣 │ ││
│ │ │ "category": "配件", │ ││
│ │ │ "min_quantity": 3, │ ││
│ │ │ "discount_rate": 0.8 │ ││
│ │ │ }, │ ││
│ │ │ { │ ││
│ │ │ "promo_id": "P004", │ ││
│ │ │ "type": "coupon", // 优惠券 │ ││
│ │ │ "code": "SAVE50", │ ││
│ │ │ "threshold": 500, │ ││
│ │ │ "reduce": 50 │ ││
│ │ │ } │ ││
│ │ │ ] │ ││
│ │ │ 80ms │ ││
│ │ └──────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 获得营销活动列表 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 5: 调用Pricing Service计算最终价格 ││
│ │ (复杂的价格计算逻辑,处理多层级优惠叠加) ││
│ │ ┌──────────────────────────────────────────────────┐ ││
│ │ │ [Pricing Service] │ ││
│ │ │ RPC: CalculateFinalPrice(items, promos) │ ││
│ │ │ ↓ │ ││
│ │ │ 计算流程(4层架构): │ ││
│ │ │ │ ││
│ │ │ 1. 商品原价计算 │ ││
│ │ │ SKU 1001: 299 × 2 = 598元 │ ││
│ │ │ SKU 1005: 89 × 1 = 89元 │ ││
│ │ │ SKU 2003: 19 × 3 = 57元 │ ││
│ │ │ 小计:744元 │ ││
│ │ │ │ ││
│ │ │ 2. 应用商品级别促销 │ ││
│ │ │ SKU 1001: 598 × 0.9 = 538.2元(9折) │ ││
│ │ │ SKU 1005: 89元(无促销) │ ││
│ │ │ SKU 2003: 57 × 0.8 = 45.6元(买3件8折) │ ││
│ │ │ 小计:672.8元 │ ││
│ │ │ │ ││
│ │ │ 3. 应用订单级别促销 │ ││
│ │ │ 满300减50:672.8 - 50 = 622.8元 │ ││
│ │ │ │ ││
│ │ │ 4. 应用优惠券 │ ││
│ │ │ 满500减50:622.8 - 50 = 572.8元 │ ││
│ │ │ │ ││
│ │ │ 最终总价:572.8元 │ ││
│ │ │ (注:运费、服务费等在确认下单时才计算) │ ││
│ │ │ ↓ │ ││
│ │ │ 返回详细价格明细: │ ││
│ │ │ { │ ││
│ │ │ "items": [ │ ││
│ │ │ { │ ││
│ │ │ "sku_id": 1001, │ ││
│ │ │ "quantity": 2, │ ││
│ │ │ "unit_price": 299.00, │ ││
│ │ │ "subtotal": 598.00, │ ││
│ │ │ "discount": 59.80, // 9折优惠 │ ││
│ │ │ "final_price": 538.20 │ ││
│ │ │ }, │ ││
│ │ │ // ... 其他商品 │ ││
│ │ │ ], │ ││
│ │ │ "subtotal": 744.00, // 商品原价合计 │ ││
│ │ │ "item_discount": 71.20, // 商品级别优惠 │ ││
│ │ │ "order_discount": 50.00, // 订单级别优惠 │ ││
│ │ │ "coupon_discount": 50.00, // 优惠券优惠 │ ││
│ │ │ "total": 572.80, // 应付总额 │ ││
│ │ │ "saved": 171.20, // 节省金额 │ ││
│ │ │ "promotions": [ // 已应用的促销 │ ││
│ │ │ { "id": "P001", "desc": "单件9折", "amount": 59.80 },│
│ │ │ { "id": "P002", "desc": "满300减50", "amount": 50.00 },│
│ │ │ { "id": "P003", "desc": "买3件8折", "amount": 11.40 },│
│ │ │ { "id": "P004", "desc": "优惠券SAVE50", "amount": 50.00 }│
│ │ │ ] │ ││
│ │ │ } │ ││
│ │ │ 120ms │ ││
│ │ └──────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ 获得最终价格明细 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Step 6: 返回试算结果(不写入任何数据,纯计算) ││
│ │ • 不创建订单 ││
│ │ • 不预占库存 ││
│ │ • 不扣券 ││
│ │ • 仅返回计算结果供用户确认 ││
│ └─────────────────────────────────────────────────────────┘│
│ ↓ │
│ [APP/Web] ← 返回试算结果 │
│ { │
│ "can_checkout": true, // 是否可下单 │
│ "total": 572.80, │
│ "saved": 171.20, │
│ "items": [...], // 商品明细 │
│ "promotions": [...], // 促销明细 │
│ "price_breakdown": { // 价格分解 │
│ "subtotal": 744.00, // 商品原价合计 │
│ "discount": 171.20, // 总优惠金额 │
│ "final": 572.80 // 应付总额 │
│ } │
│ } │
│ │
│ 总耗时:并发查询(50ms) + 营销(80ms) + 计价(100ms) = 230ms │
│ (P95 < 300ms,P99 < 500ms) │
│ │
│ 说明: │
│ • 试算只计算商品价格和营销优惠 │
│ • 运费、服务费在确认下单时再计算(需要地址信息) │
│ • 简化计算流程,提升响应速度 │
└─────────────────────────────────────────────────────────────┘

关键设计要点

1. 与购物车模式的对比
维度 购物车模式 无购物车模式(加购试算)
数据持久化 需要(Redis/MySQL,保留7天) 不需要(前端临时存储)
适用场景 传统电商、需要跨设备同步 快速结账、单次性购买(票务、充值)
用户操作 加购 → 进入购物车 → 修改 → 结算 选择商品 → 直接结算
后端服务 Cart Service(CRUD操作) Checkout Service(只有计算)
数据一致性 需要处理购物车过期、失效 无需考虑(临时数据)
系统复杂度 高(需要购物车同步、清理) 低(无状态计算)
2. 跨商品营销规则处理

关键挑战:多个商品之间的营销规则互相影响,需要按优先级计算。

营销规则分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
商品级别(Item-Level)
├─ 单品折扣(SKU Discount):某个SKU享受9折
├─ 限时购(Flash Sale):某个SKU特价
└─ 买N送M(Bundle):买2送1

品类级别(Category-Level)
├─ 品类折扣(Category Discount):配件类8折
└─ 品类满减(Category Reduce):数码类满200减20

订单级别(Order-Level)
├─ 满减(Threshold Reduce):满300减50
├─ 满折(Threshold Discount):满500打9折
└─ 阶梯折扣(Tiered Discount):满1000打8折

优惠券级别(Coupon-Level)
├─ 满减券:满500减50
├─ 折扣券:全场9折
└─ 品类券:数码类专用券

优先级计算策略(从上到下应用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// PricingService中的计算流程(加购试算场景)
func (s *PricingService) CalculateFinalPrice(items []*Item, promos []*Promotion) *PriceDetail {
// 1. 计算商品原价
subtotal := calculateSubtotal(items)

// 2. 应用商品级别促销(每个SKU独立计算)
itemDiscount := applyItemLevelPromotions(items, promos)

// 3. 应用品类级别促销(按品类分组计算)
categoryDiscount := applyCategoryLevelPromotions(items, promos)

// 4. 应用订单级别促销(全局计算)
orderDiscount := applyOrderLevelPromotions(subtotal - itemDiscount - categoryDiscount, promos)

// 5. 应用优惠券(最后应用,避免券叠加问题)
couponDiscount := applyCouponPromotions(subtotal - itemDiscount - categoryDiscount - orderDiscount, promos)

// 6. 最终总价(注:运费、服务费在确认下单时才计算)
total := subtotal - itemDiscount - categoryDiscount - orderDiscount - couponDiscount

return &PriceDetail{
Subtotal: subtotal,
ItemDiscount: itemDiscount,
CategoryDiscount: categoryDiscount,
OrderDiscount: orderDiscount,
CouponDiscount: couponDiscount,
Total: total,
Saved: itemDiscount + categoryDiscount + orderDiscount + couponDiscount,
}
}
3. 实时试算 vs 下单确认的区别
阶段 试算(Calculate) 确认下单(Confirm)
API路径 POST /checkout/calculate POST /checkout/confirm
计算内容 商品价格+营销优惠 商品价格+营销优惠+运费+服务费
库存操作 只查询,不预占 预占库存(Reserve)
优惠券 只校验,不扣减 扣减优惠券
订单创建 不创建 创建订单
数据持久化
幂等性要求 无(纯计算) 强(防重复下单)
响应时间 <500ms <1s
调用频率 高(用户多次试算) 低(一次性操作)
4. 性能优化策略

缓存策略

1
2
3
- 商品基础信息:L1+L2缓存,TTL 30分钟
- 营销规则:Redis缓存,TTL 5分钟(规则变化频繁)
- 优惠券信息:Redis缓存,TTL 10分钟

并发优化

1
2
3
并发查询:Product + Inventory(2个服务)
串行查询:Marketing → Pricing(有数据依赖)
总耗时:并发(50ms) + 营销(80ms) + 计价(100ms) = 230ms

降级策略

1
2
3
- 营销服务失败:只返回原价,不影响试算
- 库存服务失败:隐藏库存状态,但标记"库存待确认"
- 优惠券校验失败:移除该优惠券,继续计算

4.5 用户下单全链路数据流(先创单后支付模式)

核心设计模式:预占-确认 两阶段提交(2PC)

  • Phase 1: 试算(性能优先,可用快照)
  • Phase 2: 创单(锁定资源,库存预占)
  • Phase 3: 支付(用户选择,渠道计费)
  • Phase 4: 确认(资源扣减,订单完成)
  • Phase 5: 超时(资源释放,订单取消)

API接口总览

API接口 请求方 服务提供方 核心功能 关键操作 响应时间 调用时机
POST /checkout/calculate APP/Web Checkout Service
Aggregation Service
结算试算 1. 检查库存(不扣减)
2. 计算基础价格+营销优惠
3. 可使用快照数据
80-230ms Phase 1
用户点击”去结算”
POST /checkout/confirm APP/Web Checkout Service
Order Service
确认下单
创建订单
1. 库存预占(CAS操作)
2. 实时查询商品+营销
3. 创建订单(PENDING_PAYMENT)
4. 发布order.created事件
<500ms Phase 2
用户点击”提交订单”
POST /payment/calculate APP/Web Payment Service 支付前试算 1. 校验优惠券有效性
2. 计算Coin抵扣
3. 计算支付渠道费
4. 实时返回最终金额
100-200ms Phase 3a
用户选择优惠券/Coin
(防抖100ms)
POST /payment/create APP/Web Payment Service
Payment Gateway
创建支付 1. 后端重新计算金额(防篡改)
2. 预扣优惠券和Coin
3. 创建支付记录
4. 调用支付网关(支付宝/微信)
200-300ms Phase 3b
用户点击”确认支付”
POST /payment/callback 支付宝/微信 Payment Service
Order Service
支付成功回调 1. 幂等性校验
2. 确认库存扣减
3. 确认优惠券/Coin扣减
4. 更新订单状态(PAID)
5. 发布payment.paid事件
<200ms Phase 4
用户完成支付(异步通知)

内部RPC调用

RPC接口概览

RPC接口 调用方 服务提供方 核心功能 关键操作
GetProducts() Checkout Service Product Center 查询商品信息 返回商品基础信息、价格
GetPromotions() Checkout Service Marketing Service 查询营销活动 返回当前有效的营销活动
CheckStock() Checkout Service Inventory Service 检查库存 查询可用库存(不扣减)
ReserveStock() Checkout Service Inventory Service 库存预占 Redis Lua原子操作,扣减可用库存,记录预占
ConfirmReserve() Payment Service Inventory Service 确认库存扣减 删除预占记录,确认扣减
ReleaseStock() Order Timeout Job Inventory Service 释放库存 恢复可用库存,删除预占记录
ValidateCoupon() Payment Service Marketing Service 校验优惠券 校验有效性、使用条件、适用范围
ReserveCoupon() Payment Service Marketing Service 预扣优惠券 状态:AVAILABLE → RESERVED
ConfirmCoupon() Payment Service Marketing Service 确认扣减优惠券 状态:RESERVED → USED
ReleaseCoupon() Order Timeout Job Marketing Service 回退优惠券 状态:RESERVED → AVAILABLE
GetUserCoins() Payment Service Marketing Service 查询Coin余额 返回用户可用Coin数量
ReserveCoin() Payment Service Marketing Service 预扣Coin available → reserved
ConfirmCoin() Payment Service Marketing Service 确认扣减Coin reserved → used
ReleaseCoin() Order Timeout Job Marketing Service 回退Coin reserved → available
CalculateBasePrice() Checkout Service Pricing Service 计算基础价格 商品基础价格 + 营销优惠
CreateOrder() Checkout Service Order Service 创建订单 插入订单记录(PENDING_PAYMENT)
UpdateOrderStatus() Payment Service Order Service 更新订单状态 PENDING_PAYMENT → PAID → COMPLETED

RPC接口详细定义

1. Product Center - GetProducts

功能:批量查询商品信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Request
type GetProductsRequest struct {
ItemIDs []int64 `json:"item_ids"` // 商品ID列表
SKUIDs []int64 `json:"sku_ids"` // SKU ID列表
UserID int64 `json:"user_id"` // 用户ID(用于个性化价格)
ShopID int64 `json:"shop_id"` // 店铺ID
}

// Response
type GetProductsResponse struct {
Products []ProductInfo `json:"products"`
}

type ProductInfo struct {
ItemID int64 `json:"item_id"` // 商品ID
SKUID int64 `json:"sku_id"` // SKU ID
ItemName string `json:"item_name"` // 商品名称
ShopID int64 `json:"shop_id"` // 店铺ID
ShopName string `json:"shop_name"` // 店铺名称
CategoryID int64 `json:"category_id"` // 品类ID
BasePrice int64 `json:"base_price"` // 基础价格(分)
Stock int32 `json:"stock"` // 库存数量(仅展示用)
Status int32 `json:"status"` // 商品状态:1=上架,2=下架
Attributes string `json:"attributes"` // SKU属性JSON(如颜色、尺码)
}

2. Marketing Service - GetPromotions

功能:查询当前有效的营销活动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Request
type GetPromotionsRequest struct {
ItemIDs []int64 `json:"item_ids"` // 商品ID列表
UserID int64 `json:"user_id"` // 用户ID
ShopID int64 `json:"shop_id"` // 店铺ID
ChannelID int32 `json:"channel_id"` // 渠道ID(App/Web/小程序)
}

// Response
type GetPromotionsResponse struct {
Promotions []PromotionInfo `json:"promotions"`
}

type PromotionInfo struct {
PromotionID int64 `json:"promotion_id"` // 活动ID
PromotionType int32 `json:"promotion_type"` // 活动类型:1=满减,2=折扣,3=秒杀,4=买赠
ItemIDs []int64 `json:"item_ids"` // 适用商品ID列表
DiscountType int32 `json:"discount_type"` // 折扣类型:1=金额,2=百分比
DiscountValue int64 `json:"discount_value"` // 折扣值(分或千分比)
Threshold int64 `json:"threshold"` // 门槛金额(分)
StartTime int64 `json:"start_time"` // 开始时间
EndTime int64 `json:"end_time"` // 结束时间
Priority int32 `json:"priority"` // 优先级(数字越大越优先)
StackRules string `json:"stack_rules"` // 叠加规则JSON
}

3. Inventory Service - CheckStock

功能:检查库存(不扣减)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Request
type CheckStockRequest struct {
Items []StockCheckItem `json:"items"`
}

type StockCheckItem struct {
ItemID int64 `json:"item_id"` // 商品ID
SKUID int64 `json:"sku_id"` // SKU ID
Quantity int32 `json:"quantity"` // 需要数量
}

// Response
type CheckStockResponse struct {
Results []StockCheckResult `json:"results"`
}

type StockCheckResult struct {
ItemID int64 `json:"item_id"` // 商品ID
SKUID int64 `json:"sku_id"` // SKU ID
Available int32 `json:"available"` // 可用库存
IsEnough bool `json:"is_enough"` // 是否充足
ManagementType int32 `json:"management_type"` // 管理类型:1=自管理,2=供应商,3=无限
}

4. Inventory Service - ReserveStock

功能:库存预占(原子操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Request
type ReserveStockRequest struct {
OrderID int64 `json:"order_id"` // 订单ID(幂等键)
UserID int64 `json:"user_id"` // 用户ID
Items []ReserveStockItem `json:"items"` // 预占商品列表
ExpireAt int64 `json:"expire_at"` // 过期时间戳(秒)
}

type ReserveStockItem struct {
ItemID int64 `json:"item_id"` // 商品ID
SKUID int64 `json:"sku_id"` // SKU ID
Quantity int32 `json:"quantity"` // 预占数量
}

// Response
type ReserveStockResponse struct {
Success bool `json:"success"` // 是否成功
ReserveID int64 `json:"reserve_id"` // 预占记录ID
Results []ReserveStockResult `json:"results"` // 明细结果
FailedReason string `json:"failed_reason"` // 失败原因
}

type ReserveStockResult struct {
ItemID int64 `json:"item_id"` // 商品ID
SKUID int64 `json:"sku_id"` // SKU ID
ReservedQty int32 `json:"reserved_qty"` // 已预占数量
RemainingStock int32 `json:"remaining_stock"` // 剩余库存
}

5. Inventory Service - ConfirmReserve

功能:确认库存扣减(支付成功后调用)

1
2
3
4
5
6
7
8
9
10
11
// Request
type ConfirmReserveRequest struct {
OrderID int64 `json:"order_id"` // 订单ID
ReserveID int64 `json:"reserve_id"` // 预占记录ID
}

// Response
type ConfirmReserveResponse struct {
Success bool `json:"success"` // 是否成功
Message string `json:"message"` // 消息
}

6. Inventory Service - ReleaseStock

功能:释放库存(取消订单/超时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Request
type ReleaseStockRequest struct {
OrderID int64 `json:"order_id"` // 订单ID
ReserveID int64 `json:"reserve_id"` // 预占记录ID(可选)
Reason string `json:"reason"` // 释放原因:timeout/cancel/refund
}

// Response
type ReleaseStockResponse struct {
Success bool `json:"success"` // 是否成功
ReleasedItems []ReleaseStockItem `json:"released_items"` // 已释放商品
}

type ReleaseStockItem struct {
ItemID int64 `json:"item_id"` // 商品ID
SKUID int64 `json:"sku_id"` // SKU ID
ReleasedQty int32 `json:"released_qty"` // 释放数量
}

7. Marketing Service - ValidateCoupon

功能:校验优惠券有效性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Request
type ValidateCouponRequest struct {
UserID int64 `json:"user_id"` // 用户ID
CouponCode string `json:"coupon_code"` // 优惠券码
ItemIDs []int64 `json:"item_ids"` // 商品ID列表
TotalAmount int64 `json:"total_amount"` // 订单总金额(分)
}

// Response
type ValidateCouponResponse struct {
Valid bool `json:"valid"` // 是否有效
CouponID int64 `json:"coupon_id"` // 优惠券ID
DiscountType int32 `json:"discount_type"` // 折扣类型:1=金额,2=百分比
DiscountValue int64 `json:"discount_value"` // 折扣值(分或千分比)
MaxDiscount int64 `json:"max_discount"` // 最大折扣金额(分)
MinAmount int64 `json:"min_amount"` // 最低消费金额(分)
FailedReason string `json:"failed_reason"` // 失败原因
}

8. Marketing Service - ReserveCoupon

功能:预扣优惠券

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Request
type ReserveCouponRequest struct {
UserID int64 `json:"user_id"` // 用户ID
CouponCode string `json:"coupon_code"` // 优惠券码
OrderID int64 `json:"order_id"` // 订单ID(幂等键)
ExpireAt int64 `json:"expire_at"` // 过期时间戳(秒)
}

// Response
type ReserveCouponResponse struct {
Success bool `json:"success"` // 是否成功
CouponID int64 `json:"coupon_id"` // 优惠券ID
ReserveID int64 `json:"reserve_id"` // 预占记录ID
FailedReason string `json:"failed_reason"` // 失败原因
}

9. Marketing Service - ConfirmCoupon

功能:确认扣减优惠券(支付成功后调用)

1
2
3
4
5
6
7
8
9
10
11
12
// Request
type ConfirmCouponRequest struct {
OrderID int64 `json:"order_id"` // 订单ID
CouponID int64 `json:"coupon_id"` // 优惠券ID
ReserveID int64 `json:"reserve_id"` // 预占记录ID
}

// Response
type ConfirmCouponResponse struct {
Success bool `json:"success"` // 是否成功
Message string `json:"message"` // 消息
}

10. Marketing Service - ReleaseCoupon

功能:回退优惠券(取消订单/超时)

1
2
3
4
5
6
7
8
9
10
11
12
13
// Request
type ReleaseCouponRequest struct {
OrderID int64 `json:"order_id"` // 订单ID
CouponID int64 `json:"coupon_id"` // 优惠券ID
ReserveID int64 `json:"reserve_id"` // 预占记录ID
Reason string `json:"reason"` // 释放原因:timeout/cancel
}

// Response
type ReleaseCouponResponse struct {
Success bool `json:"success"` // 是否成功
Message string `json:"message"` // 消息
}

11. Marketing Service - GetUserCoins

功能:查询用户Coin余额

1
2
3
4
5
6
7
8
9
10
11
12
13
// Request
type GetUserCoinsRequest struct {
UserID int64 `json:"user_id"` // 用户ID
}

// Response
type GetUserCoinsResponse struct {
UserID int64 `json:"user_id"` // 用户ID
AvailableCoins int64 `json:"available_coins"` // 可用Coin数量
ReservedCoins int64 `json:"reserved_coins"` // 预扣Coin数量
TotalCoins int64 `json:"total_coins"` // 总Coin数量
ExpireDate int64 `json:"expire_date"` // 最近过期日期
}

12. Marketing Service - ReserveCoin

功能:预扣Coin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Request
type ReserveCoinRequest struct {
UserID int64 `json:"user_id"` // 用户ID
OrderID int64 `json:"order_id"` // 订单ID(幂等键)
Amount int64 `json:"amount"` // 预扣金额(Coin数量)
ExpireAt int64 `json:"expire_at"` // 过期时间戳(秒)
}

// Response
type ReserveCoinResponse struct {
Success bool `json:"success"` // 是否成功
ReserveID int64 `json:"reserve_id"` // 预占记录ID
ReservedCoins int64 `json:"reserved_coins"` // 已预扣数量
AvailableCoins int64 `json:"available_coins"` // 剩余可用数量
FailedReason string `json:"failed_reason"` // 失败原因
}

13. Marketing Service - ConfirmCoin

功能:确认扣减Coin(支付成功后调用)

1
2
3
4
5
6
7
8
9
10
11
// Request
type ConfirmCoinRequest struct {
OrderID int64 `json:"order_id"` // 订单ID
ReserveID int64 `json:"reserve_id"` // 预占记录ID
}

// Response
type ConfirmCoinResponse struct {
Success bool `json:"success"` // 是否成功
Message string `json:"message"` // 消息
}

14. Marketing Service - ReleaseCoin

功能:回退Coin(取消订单/超时)

1
2
3
4
5
6
7
8
9
10
11
12
// Request
type ReleaseCoinRequest struct {
OrderID int64 `json:"order_id"` // 订单ID
ReserveID int64 `json:"reserve_id"` // 预占记录ID
Reason string `json:"reason"` // 释放原因:timeout/cancel
}

// Response
type ReleaseCoinResponse struct {
Success bool `json:"success"` // 是否成功
ReleasedCoins int64 `json:"released_coins"` // 已释放Coin数量
}

15. Pricing Service - CalculateBasePrice

功能:计算订单基础价格(商品价+营销优惠)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Request
type CalculatePriceRequest struct {
UserID int64 `json:"user_id"` // 用户ID
Items []PriceItem `json:"items"` // 商品列表
Coupons []string `json:"coupons"` // 优惠券码列表
CoinAmount int64 `json:"coin_amount"` // 使用Coin数量
ShopID int64 `json:"shop_id"` // 店铺ID
}

type PriceItem struct {
ItemID int64 `json:"item_id"` // 商品ID
SKUID int64 `json:"sku_id"` // SKU ID
Quantity int32 `json:"quantity"` // 购买数量
}

// Response
type CalculatePriceResponse struct {
TotalAmount int64 `json:"total_amount"` // 总金额(分)
OriginalAmount int64 `json:"original_amount"` // 原价(分)
DiscountAmount int64 `json:"discount_amount"` // 折扣金额(分)
CouponDiscount int64 `json:"coupon_discount"` // 优惠券折扣(分)
CoinDiscount int64 `json:"coin_discount"` // Coin折扣(分)
PromotionDiscount int64 `json:"promotion_discount"` // 活动折扣(分)
PayableAmount int64 `json:"payable_amount"` // 应付金额(分)
ItemDetails []ItemPriceDetail `json:"item_details"` // 商品明细
}

type ItemPriceDetail struct {
ItemID int64 `json:"item_id"` // 商品ID
SKUID int64 `json:"sku_id"` // SKU ID
Quantity int32 `json:"quantity"` // 数量
OriginalPrice int64 `json:"original_price"` // 原价(分)
ActualPrice int64 `json:"actual_price"` // 实付价(分)
DiscountAmount int64 `json:"discount_amount"` // 折扣金额(分)
}

16. Order Service - CreateOrder

功能:创建订单

前端调用示例(完整参数说明)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 前端调用创单接口示例(React/Vue)
async function createOrder() {
const request = {
// ==================== 核心业务参数 ====================
user_id: 67890, // 用户ID(必填)
shop_id: 10001, // 店铺ID(必填)

// ==================== 商品列表 ====================
items: [
{
item_id: 50001, // 商品ID(必填)
sku_id: 500011, // SKU ID(必填)
quantity: 2, // 购买数量(必填)
// ❌ 注意:前端传递的价格仅用于展示和对比,后端会强制重新计算
expected_price: 7999, // 前端看到的单价(用于价格对比)
}
],

// ==================== 快照信息(用于价格对比)====================
// ⚠️ 重要:这些快照数据仅用于价格对比和用户体验,后端不会直接使用
snapshot: {
snapshot_id: "snap_20260415_143022", // 快照ID
snapshot_time: 1713168622, // 快照生成时间
expires_at: 1713168922, // 快照过期时间(5分钟)
expected_total: 15998, // 前端计算的总价(分)
expected_discount: 500, // 前端计算的折扣(分)
expected_payable: 15498, // 前端计算的应付金额(分)
},

// ==================== 优惠信息(可选)====================
coupon_codes: ["SAVE50"], // 优惠券码列表(可选)
coin_amount: 100, // 使用Coin数量(可选,0表示不使用)
promotion_ids: [20001], // 参与的活动ID列表(可选)

// ==================== 收货信息 ====================
shipping_address: {
receiver_name: "张三",
phone: "13800138000",
province: "广东省",
city: "深圳市",
district: "南山区",
detail: "科技园南区某大厦18楼",
zip_code: "518000",
is_default: true,
},

// ==================== 幂等性保证 ====================
idempotency_key: "order_67890_1713168660_abc123", // 幂等键(必填)
// 生成规则:`order_{user_id}_{timestamp}_{random}`

// ==================== 其他参数 ====================
remark: "请尽快发货", // 订单备注(可选)
channel_id: 1, // 渠道ID:1=App, 2=Web, 3=小程序(必填)
device_id: "device_abc123", // 设备ID(用于风控)
source: "cart", // 来源:cart=购物车, detail=详情页, activity=活动页

// ==================== 价格确认标识 ====================
price_change_confirmed: false, // 用户是否已确认价格变化(默认false)
// 当后端发现价格变化 >5% 时,会返回错误要求用户确认
// 用户确认后,前端重新请求时将此字段设为 true
};

try {
const response = await axios.post('/api/order/create', request);
console.log('订单创建成功:', response.data);
// 跳转到支付页面
window.location.href = `/payment?order_id=${response.data.order_id}`;
} catch (error) {
if (error.response.data.code === 3010) {
// 价格变化错误,提示用户
showPriceChangedDialog(error.response.data);
} else {
showError(error.response.data.message);
}
}
}

后端接口定义(Go Struct)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Request
type CreateOrderRequest struct {
// ==================== 核心业务参数 ====================
UserID int64 `json:"user_id" binding:"required"` // 用户ID
ShopID int64 `json:"shop_id" binding:"required"` // 店铺ID
Items []OrderItem `json:"items" binding:"required"` // 商品列表

// ==================== 快照信息(用于价格对比)====================
Snapshot *SnapshotInfo `json:"snapshot"` // 前端快照信息(可选)

// ==================== 优惠信息 ====================
CouponCodes []string `json:"coupon_codes"` // 优惠券码
CoinAmount int64 `json:"coin_amount"` // 使用Coin
PromotionIDs []int64 `json:"promotion_ids"` // 活动ID列表

// ==================== 收货信息 ====================
ShippingAddress *ShippingAddress `json:"shipping_address" binding:"required"` // 收货地址

// ==================== 幂等性保证 ====================
IdempotencyKey string `json:"idempotency_key" binding:"required"` // 幂等键

// ==================== 其他参数 ====================
Remark string `json:"remark"` // 订单备注
ChannelID int32 `json:"channel_id" binding:"required"` // 渠道ID
DeviceID string `json:"device_id"` // 设备ID
Source string `json:"source"` // 来源

// ==================== 价格确认标识 ====================
PriceChangeConfirmed bool `json:"price_change_confirmed"` // 价格变化已确认
}

// ==================== 嵌套结构定义 ====================

type OrderItem struct {
ItemID int64 `json:"item_id" binding:"required"` // 商品ID
SKUID int64 `json:"sku_id" binding:"required"` // SKU ID
Quantity int32 `json:"quantity" binding:"required"` // 购买数量
ExpectedPrice int64 `json:"expected_price"` // 前端期望单价(用于对比,可选)
}

// 前端快照信息(用于价格对比,后端不直接使用)
type SnapshotInfo struct {
SnapshotID string `json:"snapshot_id"` // 快照ID
SnapshotTime int64 `json:"snapshot_time"` // 快照生成时间
ExpiresAt int64 `json:"expires_at"` // 快照过期时间
ExpectedTotal int64 `json:"expected_total"` // 前端计算的总价(分)
ExpectedDiscount int64 `json:"expected_discount"` // 前端计算的折扣(分)
ExpectedPayable int64 `json:"expected_payable"` // 前端计算的应付金额(分)
}

// 收货地址
type ShippingAddress struct {
ReceiverName string `json:"receiver_name" binding:"required"` // 收货人
Phone string `json:"phone" binding:"required"` // 手机号
Province string `json:"province" binding:"required"` // 省
City string `json:"city" binding:"required"` // 市
District string `json:"district" binding:"required"` // 区
Detail string `json:"detail" binding:"required"` // 详细地址
ZipCode string `json:"zip_code"` // 邮编
IsDefault bool `json:"is_default"` // 是否默认地址
}

// Response
type CreateOrderResponse struct {
Success bool `json:"success"` // 是否成功
OrderID int64 `json:"order_id"` // 订单ID
OrderNo string `json:"order_no"` // 订单号(用于展示)
ExpireAt int64 `json:"expire_at"` // 订单过期时间(Unix秒)
PayableAmount int64 `json:"payable_amount"` // 最终应付金额(分)
Message string `json:"message"` // 消息

// 价格变化相关
PriceChanged bool `json:"price_changed"` // 价格是否发生变化
PriceDiff int64 `json:"price_diff"` // 价格差异(分)
PriceChangeReason string `json:"price_change_reason"` // 价格变化原因
}

参数详细说明

参数组 参数名 类型 必填 说明 示例
核心参数 user_id int64 用户ID 67890
shop_id int64 店铺ID 10001
items array 商品列表(至少1个) 见下方
idempotency_key string 幂等键,防重复下单 order_67890_1713168660_abc123
商品信息 items[].item_id int64 商品ID 50001
items[].sku_id int64 SKU ID 500011
items[].quantity int32 购买数量 2
items[].expected_price int64 ⚠️ 可选 前端期望单价(用于价格对比) 7999
快照信息 snapshot object ⚠️ 可选 前端快照信息(仅用于价格对比) 见下方
snapshot.snapshot_id string - 快照ID snap_20260415_143022
snapshot.expected_total int64 - 前端计算的总价(分) 15998
snapshot.expected_payable int64 - 前端计算的应付金额(分) 15498
优惠信息 coupon_codes array ⚠️ 可选 优惠券码列表 ["SAVE50"]
coin_amount int64 ⚠️ 可选 使用Coin数量(0=不使用) 100
promotion_ids array ⚠️ 可选 活动ID列表 [20001]
收货信息 shipping_address object 收货地址 见下方
shipping_address.receiver_name string 收货人姓名 “张三”
shipping_address.phone string 手机号 “13800138000”
shipping_address.province string “广东省”
shipping_address.city string “深圳市”
shipping_address.district string “南山区”
shipping_address.detail string 详细地址 “科技园南区某大厦18楼”
幂等性 idempotency_key string 幂等键(生成规则见下方) order_67890_1713168660_abc123
其他 remark string 订单备注 “请尽快发货”
channel_id int32 渠道ID(1=App, 2=Web, 3=小程序) 1
device_id string ⚠️ 可选 设备ID(用于风控) “device_abc123”
source string ⚠️ 可选 来源(cart/detail/activity) “cart”
价格确认 price_change_confirmed bool 用户是否已确认价格变化 false

关键参数说明

1. 幂等键(idempotency_key)

生成规则

1
2
3
4
5
6
7
8
9
10
// 前端生成幂等键
function generateIdempotencyKey(userId) {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 10);
return `order_${userId}_${timestamp}_${random}`;
}

// 示例
const key = generateIdempotencyKey(67890);
// 结果:order_67890_1713168660_abc123xyz

作用

  • 防止用户重复点击”提交订单”按钮,导致重复创建订单
  • 后端使用此键进行幂等性校验(同一个key只创建一次订单)
  • 有效期通常为24小时

前端处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 生成幂等键并缓存(单次下单流程中不变)
const idempotencyKey = generateIdempotencyKey(userId);
localStorage.setItem('current_idempotency_key', idempotencyKey);

// 提交订单时使用
async function submitOrder() {
const key = localStorage.getItem('current_idempotency_key');
await createOrder({ ...orderData, idempotency_key: key });
}

// 订单创建成功后清除
function onOrderSuccess() {
localStorage.removeItem('current_idempotency_key');
}

2. 快照信息(snapshot)

作用

  • 性能优化:前端可缓存快照,减少试算时的重复计算
  • 价格对比:后端对比前端期望价格与实际价格,差异过大时提示用户
  • 不可信:后端不会直接使用快照中的价格,会强制实时查询和计算

数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
详情页 → 生成快照(5分钟有效)

购物车 → 携带快照(可能已过期)

试算接口 → 判断快照是否过期
├─ 未过期 → 使用快照数据(性能优化)
└─ 已过期 → 重新查询(保证准确性)

创单接口 → ❌ 不使用快照,强制实时查询

价格对比 → 对比前端期望价格 vs 后端实际价格
├─ 差异 < 5% → 允许创单
└─ 差异 >= 5% → 返回错误,要求用户确认

3. 价格确认标识(price_change_confirmed)

使用场景:价格变化超过阈值时的二次确认

流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 第一次提交(price_change_confirmed = false)
try {
await createOrder({ ...orderData, price_change_confirmed: false });
} catch (error) {
if (error.code === 3010) { // 价格变化错误
// 展示价格变化弹窗
showDialog({
title: '价格已变化',
message: `商品价格已从 ¥${error.expected_price} 变为 ¥${error.actual_price}`,
onConfirm: () => {
// 用户确认后,重新提交(price_change_confirmed = true)
createOrder({ ...orderData, price_change_confirmed: true });
}
});
}
}

4. 前端不需要传递的参数

以下参数由后端强制实时查询,前端无需传递(即使传递也会被忽略):

不需要传递 原因
total_amount 后端实时计算,防止前端篡改
discount_amount 后端实时计算,防止前端篡改
payable_amount 后端实时计算,防止前端篡改
item_name 后端从商品服务实时查询
original_price 后端从商品服务实时查询
actual_price 后端从计价服务实时计算
promotion_info 后端从营销服务实时查询

为什么?

  • 安全性:防止用户通过抓包修改价格,造成资损
  • 准确性:确保使用最新的商品价格和活动信息
  • 一致性:所有价格计算由后端统一控制

前端创单参数快速参考

必填参数(7个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"user_id": 67890,
"shop_id": 10001,
"items": [{
"item_id": 50001,
"sku_id": 500011,
"quantity": 2
}],
"shipping_address": {
"receiver_name": "张三",
"phone": "13800138000",
"province": "广东省",
"city": "深圳市",
"district": "南山区",
"detail": "科技园南区某大厦18楼"
},
"idempotency_key": "order_67890_1713168660_abc123",
"channel_id": 1
}

可选参数(优惠相关)

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"coupon_codes": ["SAVE50"], // 优惠券
"coin_amount": 100, // Coin抵扣
"promotion_ids": [20001], // 活动ID
"snapshot": { // 快照信息(用于价格对比)
"snapshot_id": "snap_20260415_143022",
"expected_payable": 15498
},
"remark": "请尽快发货", // 订单备注
"device_id": "device_abc123", // 设备ID(风控)
"source": "cart", // 来源
"price_change_confirmed": false // 价格变化确认
}

前端实现清单

  • ✅ 生成并缓存幂等键(idempotency_key)
  • ✅ 收集商品信息(item_id, sku_id, quantity)
  • ✅ 收集收货地址(完整的地址信息)
  • ✅ 收集优惠券码(如果用户选择了优惠券)
  • ✅ 收集快照信息(expected_payable用于价格对比)
  • ✅ 实现价格变化二次确认逻辑
  • ❌ 不要传递价格相关字段(total_amount, discount_amount等)

完整JSON请求示例

场景1:基础订单(无优惠)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
"user_id": 67890,
"shop_id": 10001,
"items": [
{
"item_id": 50001,
"sku_id": 500011,
"quantity": 2,
"expected_price": 799900
},
{
"item_id": 50002,
"sku_id": 500021,
"quantity": 1,
"expected_price": 299900
}
],
"shipping_address": {
"receiver_name": "张三",
"phone": "13800138000",
"province": "广东省",
"city": "深圳市",
"district": "南山区",
"detail": "科技园南区腾讯大厦18楼",
"zip_code": "518000",
"is_default": true
},
"idempotency_key": "order_67890_1713168660_abc123xyz",
"channel_id": 1,
"device_id": "device_ios_abc123456789",
"source": "cart",
"remark": "",
"snapshot": {
"snapshot_id": "snap_20260415_143022_xyz",
"snapshot_time": 1713168622,
"expires_at": 1713168922,
"expected_total": 189970000,
"expected_discount": 0,
"expected_payable": 189970000
},
"coupon_codes": [],
"coin_amount": 0,
"promotion_ids": [],
"price_change_confirmed": false
}

场景2:使用优惠券 + Coin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"user_id": 123456,
"shop_id": 10005,
"items": [
{
"item_id": 60001,
"sku_id": 600012,
"quantity": 1,
"expected_price": 299900
}
],
"shipping_address": {
"receiver_name": "李四",
"phone": "13900139000",
"province": "北京市",
"city": "北京市",
"district": "海淀区",
"detail": "中关村软件园1号楼A座2层",
"zip_code": "100089",
"is_default": false
},
"idempotency_key": "order_123456_1713168700_def456uvw",
"channel_id": 1,
"device_id": "device_android_def456789012",
"source": "detail",
"remark": "请在工作日送货,上班时间9:00-18:00",
"snapshot": {
"snapshot_id": "snap_20260415_143100_uvw",
"snapshot_time": 1713168660,
"expires_at": 1713168960,
"expected_total": 29990000,
"expected_discount": 5100,
"expected_payable": 29939000
},
"coupon_codes": ["SAVE50"],
"coin_amount": 100,
"promotion_ids": [20001],
"price_change_confirmed": false
}

场景3:秒杀活动订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"user_id": 789012,
"shop_id": 10008,
"items": [
{
"item_id": 70001,
"sku_id": 700015,
"quantity": 1,
"expected_price": 199900
}
],
"shipping_address": {
"receiver_name": "王五",
"phone": "13700137000",
"province": "上海市",
"city": "上海市",
"district": "浦东新区",
"detail": "张江高科技园区祖冲之路1000号",
"zip_code": "201203",
"is_default": true
},
"idempotency_key": "order_789012_1713168750_ghi789rst",
"channel_id": 1,
"device_id": "device_ios_ghi789012345",
"source": "activity",
"remark": "秒杀商品,请尽快发货",
"snapshot": {
"snapshot_id": "snap_20260415_143200_rst",
"snapshot_time": 1713168720,
"expires_at": 1713169020,
"expected_total": 19990000,
"expected_discount": 10000000,
"expected_payable": 9990000
},
"coupon_codes": [],
"coin_amount": 0,
"promotion_ids": [30001],
"price_change_confirmed": false
}

场景4:多商品 + 多优惠 + 价格变化已确认

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
"user_id": 345678,
"shop_id": 10003,
"items": [
{
"item_id": 80001,
"sku_id": 800011,
"quantity": 2,
"expected_price": 499900
},
{
"item_id": 80002,
"sku_id": 800022,
"quantity": 3,
"expected_price": 199900
},
{
"item_id": 80003,
"sku_id": 800033,
"quantity": 1,
"expected_price": 899900
}
],
"shipping_address": {
"receiver_name": "赵六",
"phone": "13600136000",
"province": "浙江省",
"city": "杭州市",
"district": "西湖区",
"detail": "文三路某某大厦B座10层1001室",
"zip_code": "310012",
"is_default": false
},
"idempotency_key": "order_345678_1713168800_jkl012mno",
"channel_id": 2,
"device_id": "device_web_jkl012345678",
"source": "cart",
"remark": "包装要结实,商品贵重",
"snapshot": {
"snapshot_id": "snap_20260415_143250_mno",
"snapshot_time": 1713168770,
"expires_at": 1713169070,
"expected_total": 249870000,
"expected_discount": 30000000,
"expected_payable": 219870000
},
"coupon_codes": ["SAVE100", "VIP2024"],
"coin_amount": 500,
"promotion_ids": [20002, 20003],
"price_change_confirmed": true
}

字段值说明

字段 格式 示例值 说明
user_id int64 67890 用户ID
shop_id int64 10001 店铺ID
item_id int64 50001 商品ID
sku_id int64 500011 SKU ID
quantity int32 2 购买数量
expected_price int64 799900 单价(分),7999元 = 799900分
phone string “13800138000” 11位手机号
idempotency_key string “order_67890_1713168660_abc123” order_{user_id}_{timestamp}_{random}
channel_id int32 1 1=App, 2=Web, 3=小程序
device_id string “device_ios_abc123” 设备唯一标识
source string “cart” cart/detail/activity
snapshot_time int64 1713168622 Unix时间戳(秒)
expected_total int64 189970000 总价(分),18997元 = 18997000分
coupon_codes array [“SAVE50”] 优惠券码数组,空数组=不使用
coin_amount int64 100 使用Coin数量,0=不使用
promotion_ids array [20001] 活动ID数组,空数组=不参与
price_change_confirmed bool false 首次提交=false,二次确认=true

价格单位说明(重要!)

1
2
3
4
5
6
7
8
9
10
11
// 前端展示:¥79.99
// 后端传递:799900(分)

// 转换公式
const priceInCents = Math.round(priceInYuan * 100); // 元转分
const priceInYuan = priceInCents / 100; // 分转元

// 示例
7999 元 → 799900
299.9 元 → 29990
0.01 元 → 1

为什么使用”分”作为单位?

  • ✅ 避免浮点数精度问题(0.1 + 0.2 ≠ 0.3)
  • ✅ 整数运算更快更准确
  • ✅ 金融系统标准做法

17. Order Service - UpdateOrderStatus

功能:更新订单状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Request
type UpdateOrderStatusRequest struct {
OrderID int64 `json:"order_id"` // 订单ID
CurrentStatus int32 `json:"current_status"` // 当前状态(乐观锁)
TargetStatus int32 `json:"target_status"` // 目标状态
Operator string `json:"operator"` // 操作人
Reason string `json:"reason"` // 原因
}

// Response
type UpdateOrderStatusResponse struct {
Success bool `json:"success"` // 是否成功
Message string `json:"message"` // 消息
}

RPC调用约定

  1. 幂等性:所有写操作(Reserve/Confirm/Release)必须使用 order_id 作为幂等键
  2. 超时设置
    • 查询类接口:500ms
    • 写入类接口:1s
    • 确认类接口:2s
  3. 重试策略
    • 幂等接口:最多重试3次(指数退避:100ms, 200ms, 400ms)
    • 非幂等接口:不重试,直接失败
  4. 错误码
    • 2000:参数错误
    • 3001:库存不足
    • 3002:优惠券不可用
    • 3003:Coin余额不足
    • 5000:系统错误

Kafka事件

Topic 发布者 订阅者 触发时机 消息内容
order.created Checkout Service Cart, Search, Analytics, Notification 订单创建成功 order_id, user_id, items, amount, status
payment.paid Payment Service Order, Supplier Gateway, Notification, Analytics 支付成功 order_id, payment_id, amount, paid_at
order.cancelled Order Timeout Job Inventory, Marketing, Notification 订单超时取消 order_id, reason, cancelled_at

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
用户操作流程:浏览商品 → 加购 → 试算 → 【创建订单】 → 【选择支付】 → 【完成支付】 → 履约

═══════════════════════════════════════════════════════════════
Phase 1: 试算计价(同步,性能优先,可用快照数据)
═══════════════════════════════════════════════════════════════

[APP/Web] 用户点击"去结算"
↓ POST /checkout/calculate
↓ {user_id, items: [{sku_id, quantity, snapshot}]}

[Checkout Service/Aggregation Service]

├─ Step 1: 判断快照是否过期
│ ├─ 快照未过期 → 使用快照数据(商品信息、营销活动)
│ └─ 快照过期 → 实时查询 Product + Marketing

├─ Step 2: 实时查询库存(必须实时,不能用快照)
│ └─→ [Inventory Service] RPC: CheckStock()
│ └─ SELECT available FROM stock WHERE sku_id=?
│ └─ 返回:available=10(检查不扣减)

├─ Step 3: 调用计价服务(仅基础价格+营销)
│ └─→ [Pricing Service] RPC: CalculateBasePrice()
│ ├─ 商品基础价格:299.00
│ ├─ 满减优惠:-30.00
│ └─ 返回:base_price=299.00, discount=30.00

└─ Step 4: 返回试算结果

[APP/Web] ← 返回试算详情(总耗时:80-230ms)
{
"items": [...],
"base_price": 299.00,
"discount": 30.00,
"amount_to_pay": 269.00, // 待支付金额(未含支付渠道费)
"available_coupons": [...] // 可用优惠券列表
}

═══════════════════════════════════════════════════════════════
Phase 2: 创建订单(同步 + 异步混合,锁定资源)
═══════════════════════════════════════════════════════════════

[APP/Web] 用户点击"提交订单"
↓ POST /checkout/confirm
↓ {user_id, items: [{sku_id, quantity}]}

[Checkout Service] - Saga 协调者

┌────────────────────────────────────────────────┐
│ Step 1: 实时查询商品和营销(不使用快照) │
├────────────────────────────────────────────────┤
│ 并发调用: │
│ ├─→ [Product Center] RPC: GetProducts() │
│ │ └─ 返回:商品基础信息 │
│ └─→ [Marketing Service] RPC: GetPromotions()│
│ └─ 返回:当前有效营销活动(实时校验) │
│ 耗时:100ms │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 2: 库存预占(同步,必须成功)✅ │
├────────────────────────────────────────────────┤
│ [Inventory Service] RPC: ReserveStock() │
│ ↓ Redis Lua 原子操作(CAS) │
│ ↓ local available = redis.call('GET', key) │
│ ↓ if available >= quantity then │
│ ↓ redis.call('DECRBY', key, quantity) │
│ ↓ redis.call('SET', reserve_key, data, 'EX', 900) │
│ ↓ return reserve_id │
│ ↓ else return nil end │
│ ↓ │
│ └─ 返回:reserve_ids = ["rsv_001", ...] │
│ └─ 过期时间:15分钟(900秒) │
│ │
│ 库存变化: │
│ stock:available:1001 = 10 → 8(扣减2) │
│ stock:reserved:1001:rsv_001 = { │
│ quantity: 2, │
│ order_id: null, │
│ expires_at: 1744634100 │
│ } TTL=900秒 │
│ 耗时:50ms │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 3: 计算订单金额(基础价格+营销) │
├────────────────────────────────────────────────┤
│ [Pricing Service] RPC: CalculateBasePrice() │
│ ├─ 商品基础价格:299.00 × 2 = 598.00 │
│ ├─ 满减优惠:-60.00 │
│ └─ 返回:base_price=598.00, discount=60.00 │
│ │
│ 注意:此时不计算支付渠道费(支付时计算) │
│ 耗时:80ms │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 4: 创建订单(同步)✅ │
├────────────────────────────────────────────────┤
│ [Order Service] RPC: CreateOrder() │
│ ↓ INSERT INTO order_tab VALUES ( │
│ order_id = 1001, │
│ user_id = 67890, │
│ status = 'PENDING_PAYMENT', ← 关键状态 │
│ base_price = 598.00, │
│ discount = 60.00, │
│ amount_to_pay = 538.00, │
│ reserve_ids = '["rsv_001"]', │
│ pay_expire_at = NOW() + 15分钟, │
│ created_at = NOW() │
│ ) │
│ ↓ │
│ └─ 返回:order_id = 1001 │
│ 耗时:100ms │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 5: 发布 order.created 事件(异步) │
├────────────────────────────────────────────────┤
│ [Kafka Topic: order.created] │
│ Payload: { │
│ order_id: 1001, │
│ user_id: 67890, │
│ items: [{sku_id, quantity}], │
│ amount: 538.00, │
│ status: 'PENDING_PAYMENT', │
│ reserve_ids: ["rsv_001"] │
│ } │
│ ↓ │
│ ├─→ [Cart Service] 监听:清理购物车 │
│ ├─→ [Search Service] 监听:更新销量(+1) │
│ ├─→ [Analytics Service] 监听:订单漏斗分析 │
│ └─→ [Notification Service] 监听:订单确认通知│
└────────────────────────────────────────────────┘

[APP/Web] ← 返回订单信息(总耗时:<500ms)
{
"order_id": 1001,
"base_price": 598.00,
"discount": 60.00,
"amount_to_pay": 538.00,
"pay_expire_at": 1744634100, // 15分钟后过期
"status": "PENDING_PAYMENT",
"reserved": true // 库存已预占
}

═══════════════════════════════════════════════════════════════
Phase 3a: 支付页面试算(实时计算最终金额)✅ 关键
═══════════════════════════════════════════════════════════════

[APP/Web] 用户点击"去支付",进入支付页面

┌─────────────────────────────────────────────┐
│ 支付页面展示: │
│ • 订单金额:538.00 │
│ • 可用优惠券列表(实时查询) │
│ • 可用Coin余额:100 │
│ • 支付方式选择(支付宝、微信、银行卡) │
│ • 最终支付金额:待计算 │
└─────────────────────────────────────────────┘

用户交互:选择/修改优惠券、Coin、支付渠道
↓ 每次选择变化时,触发实时试算(防抖100ms)

↓ POST /payment/calculate(支付前试算)
↓ {
↓ order_id: 1001,
↓ coupon_id: "CPN001", // 用户选择的优惠券
↓ coin_amount: 50, // 用户选择使用50个Coin
↓ payment_channel: "alipay" // 用户选择的支付渠道
↓ }

[Payment Service]

┌────────────────────────────────────────────────┐
│ Step 1: 查询订单基础金额 │
├────────────────────────────────────────────────┤
│ [Order Service] RPC: GetOrder(1001) │
│ ↓ 校验订单状态 │
│ ├─ status == 'PENDING_PAYMENT' ✓ │
│ ├─ pay_expire_at > NOW() ✓ │
│ └─ 返回:order { │
│ order_id: 1001, │
│ amount_to_pay: 538.00, ← 订单基础金额 │
│ items: [{sku_id, quantity, price}] │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 2: 校验并计算优惠券抵扣 │
├────────────────────────────────────────────────┤
│ if coupon_id != null { │
│ [Marketing Service] RPC: ValidateCoupon() │
│ ↓ 校验优惠券: │
│ ├─ 是否有效(未过期、未使用) │
│ ├─ 是否满足使用条件(满300减50) │
│ ├─ 是否适用当前订单(品类限制) │
│ └─ 返回:{ │
│ coupon_id: "CPN001", │
│ type: "满减", │
│ condition: 300, │
│ discount: 50 │
│ } │
│ │
│ 计算抵扣金额: │
│ if order.amount_to_pay >= 300 { │
│ coupon_discount = 50.00 │
│ } │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 3: 校验并计算Coin抵扣 │
├────────────────────────────────────────────────┤
│ if coin_amount > 0 { │
│ [Marketing Service] RPC: GetUserCoins() │
│ ↓ 查询用户Coin余额 │
│ ├─ 可用余额:100 │
│ ├─ 本次使用:50(用户输入) │
│ └─ 校验:50 <= 100 ✓ │
│ │
│ 计算Coin抵扣金额: │
│ coin_discount = coin_amount * 0.01 │
│ = 50 * 0.01 = 0.50 │
│ // 通常1 Coin = 0.01元 │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 4: 计算支付渠道费 │
├────────────────────────────────────────────────┤
│ channel_fee = calculateChannelFee( │
│ payment_channel: 'alipay', │
│ amount: 538.00 │
│ ) │
│ │
│ 渠道费率规则: │
│ • 支付宝/微信:0% │
│ • 信用卡:1% │
│ • 花呗分期3期:2% │
│ • 花呗分期6期:4% │
│ │
│ channel_fee = 0.00(支付宝无手续费) │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 5: 计算最终支付金额(显示给用户) │
├────────────────────────────────────────────────┤
│ final_amount = order.amount_to_pay │
│ - coupon_discount │
│ - coin_discount │
│ + channel_fee │
│ = 538.00 - 50.00 - 0.50 + 0.00 │
│ = 487.50 │
│ │
│ 价格明细: │
│ 订单金额:538.00 │
│ 优惠券: -50.00 │
│ Coin抵扣:-0.50 │
│ 渠道费: +0.00 │
│ ────────────── │
│ 实付金额:487.50 ✅ │
└────────────────────────────────────────────────┘

[APP/Web] ← 返回试算结果(实时更新页面)
{
"order_id": 1001,
"base_amount": 538.00,
"coupon_discount": 50.00,
"coin_discount": 0.50,
"channel_fee": 0.00,
"final_amount": 487.50, ← 用户看到的最终金额
"breakdown": {
"订单金额": "¥538.00",
"优惠券": "-¥50.00",
"Coin抵扣": "-¥0.50",
"支付渠道费": "+¥0.00"
},
"remaining_coin": 50 // 使用后剩余Coin
}

用户在支付页面看到实时更新的价格:
┌─────────────────────────────────────────────┐
│ 支付详情 │
│ │
│ 订单金额 ¥538.00 │
│ 优惠券 CPN001 -¥50.00 ← 用户选择 │
│ Coin抵扣(50) -¥0.50 ← 用户选择 │
│ 支付渠道费 ¥0.00 ← 自动计算 │
│ ───────────────────── │
│ 实付金额 ¥487.50 ← 实时更新 ✅ │
│ │
│ [确认支付] 按钮 │
└─────────────────────────────────────────────┘

═══════════════════════════════════════════════════════════════
Phase 3b: 确认支付(用户点击"确认支付")
═══════════════════════════════════════════════════════════════

[APP/Web] 用户点击"确认支付"
↓ POST /payment/create(创建支付记录)
↓ {
↓ order_id: 1001,
↓ payment_channel: "alipay",
↓ coupon_id: "CPN001",
↓ coin_amount: 50,
↓ expected_amount: 487.50 ← 前端试算的金额(防篡改校验)
↓ }

[Payment Service]

┌────────────────────────────────────────────────┐
│ Step 1: 重新计算最终金额(后端校验)✅ │
├────────────────────────────────────────────────┤
│ // 后端必须重新计算,不能信任前端传来的金额 │
│ actual_amount = recalculate( │
│ order_id, coupon_id, coin_amount, channel │
│ ) │
│ │
│ // 校验前端金额与后端计算是否一致 │
│ if abs(actual_amount - expected_amount) > 0.01 {│
│ return Error("价格已变化,请重新确认") │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 2: 预扣优惠券和Coin(支付成功后确认) │
├────────────────────────────────────────────────┤
│ if coupon_id != null { │
│ [Marketing Service] RPC: ReserveCoupon() │
│ UPDATE coupon_user_log SET │
│ status = 'RESERVED', ← 预扣状态 │
│ order_id = 1001, │
│ reserved_at = NOW() │
│ } │
│ │
│ if coin_amount > 0 { │
│ [Marketing Service] RPC: ReserveCoin() │
│ UPDATE user_coin SET │
│ available = available - 50, ← 预扣 │
│ reserved = reserved + 50 │
│ WHERE user_id = 67890 │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 3: 创建支付记录 │
├────────────────────────────────────────────────┤
│ INSERT INTO payment VALUES ( │
│ payment_id = 2001, │
│ order_id = 1001, │
│ payment_channel = 'alipay', │
│ base_amount = 538.00, │
│ coupon_discount = 50.00, │
│ coin_discount = 0.50, │
│ channel_fee = 0.00, │
│ final_amount = 487.50, ← 最终金额 │
│ status = 'PENDING', │
│ created_at = NOW() │
│ ) │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 4: 调用支付网关 │
├────────────────────────────────────────────────┤
│ [Payment Gateway] RPC: CreatePay() │
│ ↓ 调用支付宝/微信API │
│ ↓ 参数:{ │
│ amount: 487.50, ← 最终支付金额 │
│ order_no: "ORD1001", │
│ callback_url: "https://api.com/callback" │
│ } │
│ └─ 返回:pay_url(支付页面URL) │
└────────────────────────────────────────────────┘

[APP/Web] ← 返回支付URL
{
"payment_id": 2001,
"pay_url": "https://alipay.com/...",
"final_amount": 487.50,
"qr_code": "data:image/png;base64,..."
}

[APP/Web] 跳转到支付页面(或显示二维码)

用户在支付宝/微信完成支付...

**关键设计说明**:
1. ✅ **支付前试算**:用户每次选择优惠券/Coin/支付渠道时,实时试算最终金额
2. ✅ **防抖优化**:试算接口100ms防抖,避免频繁调用
3. ✅ **后端校验**:创建支付时,后端必须重新计算金额,不信任前端
4. ✅ **预扣机制**:优惠券和Coin在创建支付时预扣,支付成功后确认扣减
5. ✅ **价格透明**:用户清楚看到每一项优惠的明细

═══════════════════════════════════════════════════════════════
Phase 4: 支付成功回调(异步通知,确认资源扣减)
═══════════════════════════════════════════════════════════════

[支付宝/微信] 支付成功
↓ POST /payment/callback(异步回调)
↓ {payment_id, trade_no, amount, ...}

[Payment Service]

┌────────────────────────────────────────────────┐
│ Step 1: 幂等性校验 │
├────────────────────────────────────────────────┤
│ if isDuplicate(payment_id) { │
│ return SUCCESS // 重复回调,直接返回 │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 2: 更新支付状态 │
├────────────────────────────────────────────────┤
│ UPDATE payment SET │
│ status = 'PAID', │
│ paid_at = NOW(), │
│ trade_no = '支付宝流水号' │
│ WHERE payment_id = 2001 │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 3: 更新订单状态 ✅ │
├────────────────────────────────────────────────┤
│ [Order Service] RPC: UpdateOrderStatus() │
│ UPDATE order_tab SET │
│ status = 'PAID', ← 从 PENDING_PAYMENT 变更│
│ paid_at = NOW() │
│ WHERE order_id = 1001 │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 4: 确认库存扣减(从预占转为实际扣减)✅ │
├────────────────────────────────────────────────┤
│ [Inventory Service] RPC: ConfirmReserve() │
│ ↓ Redis 操作 │
│ ↓ // 删除预占记录(已不需要) │
│ ↓ redis.call('DEL', 'stock:reserved:1001:rsv_001')│
│ ↓ │
│ ↓ // 库存已在预占时扣减,这里仅确认 │
│ ↓ // stock:available:1001 = 8(已扣减) │
│ ↓ │
│ └─ 返回:SUCCESS │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 5: 确认优惠券和Coin扣减(如果有)✅ │
├────────────────────────────────────────────────┤
│ // 优惠券:从预扣状态转为确认扣减 │
│ if coupon_id != null { │
│ [Marketing Service] RPC: ConfirmCoupon() │
│ UPDATE coupon_user_log SET │
│ status = 'USED', ← 从RESERVED变为USED │
│ order_id = 1001, │
│ used_at = NOW() │
│ WHERE coupon_id = ? AND user_id = ? │
│ } │
│ │
│ // Coin:从预扣状态转为确认扣减 │
│ if coin_amount > 0 { │
│ [Marketing Service] RPC: ConfirmCoin() │
│ UPDATE user_coin SET │
│ reserved = reserved - 50, │
│ used = used + 50 ← 确认扣减 │
│ WHERE user_id = 67890 │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 6: 发布 payment.paid 事件(异步) │
├────────────────────────────────────────────────┤
│ [Kafka Topic: payment.paid] │
│ Payload: { │
│ order_id: 1001, │
│ payment_id: 2001, │
│ amount: 523.38, │
│ paid_at: 1744633800 │
│ } │
│ ↓ │
│ ├─→ [Supplier Gateway] 监听:提交供应商订单 │
│ ├─→ [Notification Service] 监听:支付成功通知│
│ ├─→ [Analytics Service] 监听:GMV统计 │
│ └─→ [Risk Service] 监听:风控检测 │
└────────────────────────────────────────────────┘

═══════════════════════════════════════════════════════════════
Phase 5: 供应商履约(异步)
═══════════════════════════════════════════════════════════════

[Supplier Gateway] 监听到 payment.paid 事件

┌────────────────────────────────────────────────┐
│ Step 1: 调用供应商API创建订单 │
├────────────────────────────────────────────────┤
│ [Supplier API] POST /create_order │
│ ↓ {sku_id, quantity, user_info, ...} │
│ ↓ 供应商侧处理... │
│ └─ 返回:supplier_order_id = "S123456" │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 2: 轮询供应商订单状态 │
├────────────────────────────────────────────────┤
│ 每隔10秒查询一次,最多查询60次(10分钟) │
│ [Supplier API] GET /query_order │
│ ↓ {supplier_order_id: "S123456"} │
│ └─ 返回:status = "SUCCESS", voucher_code │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 3: 发布 order.fulfilled 事件 │
├────────────────────────────────────────────────┤
│ [Kafka Topic: order.fulfilled] │
│ Payload: { │
│ order_id: 1001, │
│ supplier_order_id: "S123456", │
│ status: "COMPLETED", │
│ voucher_code: "ABC123XYZ" │
│ } │
│ ↓ │
│ ├─→ [Order Service] 监听:更新订单状态→COMPLETED│
│ ├─→ [Notification Service] 监听:发送券码/凭证│
│ └─→ [Analytics Service] 监听:履约成功率统计 │
└────────────────────────────────────────────────┘

═══════════════════════════════════════════════════════════════
Phase 6: 超时未支付处理(定时任务)
═══════════════════════════════════════════════════════════════

[Order Timeout Job] 定时扫描(每分钟执行一次)

┌────────────────────────────────────────────────┐
│ Step 1: 扫描超时未支付订单 │
├────────────────────────────────────────────────┤
│ SELECT * FROM order_tab WHERE │
│ status = 'PENDING_PAYMENT' │
│ AND pay_expire_at < NOW() │
│ LIMIT 1000 │
│ ↓ │
│ └─ 返回:[order_1001, order_1002, ...] │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 2: 释放库存(从预占状态释放)✅ │
├────────────────────────────────────────────────┤
│ for each order { │
│ [Inventory Service] RPC: ReleaseStock() │
│ ↓ Redis 操作 │
│ ↓ // 读取预占记录 │
│ ↓ reserve = redis.call('GET', reserve_key) │
│ ↓ quantity = reserve.quantity │
│ ↓ │
│ ↓ // 恢复可用库存 │
│ ↓ redis.call('INCRBY', available_key, quantity)│
│ ↓ // stock:available:1001 = 8 → 10 │
│ ↓ │
│ ↓ // 删除预占记录 │
│ ↓ redis.call('DEL', reserve_key) │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 3: 回退优惠券和Coin(如果有预扣) │
├────────────────────────────────────────────────┤
│ // 查询订单关联的支付记录 │
│ payment = getPaymentByOrderID(order_id) │
│ │
│ // 回退优惠券(从RESERVED回到AVAILABLE) │
│ if payment.coupon_id != null { │
│ [Marketing Service] RPC: ReleaseCoupon() │
│ UPDATE coupon_user_log SET │
│ status = 'AVAILABLE', ← 回退预扣 │
│ order_id = NULL, │
│ reserved_at = NULL │
│ WHERE coupon_id = ? AND user_id = ? │
│ } │
│ │
│ // 回退Coin(从reserved回到available) │
│ if payment.coin_amount > 0 { │
│ [Marketing Service] RPC: ReleaseCoin() │
│ UPDATE user_coin SET │
│ available = available + 50, ← 回退 │
│ reserved = reserved - 50 │
│ WHERE user_id = 67890 │
│ } │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 4: 更新订单状态 │
├────────────────────────────────────────────────┤
│ UPDATE order_tab SET │
│ status = 'CANCELLED', ← 从 PENDING_PAYMENT │
│ cancel_reason = '超时未支付', │
│ cancelled_at = NOW() │
│ WHERE order_id = ? │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Step 5: 发布 order.cancelled 事件 │
├────────────────────────────────────────────────┤
│ [Kafka Topic: order.cancelled] │
│ Payload: { │
│ order_id: 1001, │
│ reason: '超时未支付', │
│ cancelled_at: 1744634100 │
│ } │
│ ↓ │
│ ├─→ [Analytics Service] 监听:订单漏斗分析 │
│ └─→ [Notification Service] 监听:取消通知 │
└────────────────────────────────────────────────┘

关键数据流说明(完整资源状态变化)

阶段 操作 库存状态 订单状态 支付状态 优惠券/Coin 耗时
Phase 1: 试算 检查库存(不扣减) available=10 - - 查询可用优惠券/Coin 80-230ms
Phase 2: 创单 库存预占 available=8
reserved=2
PENDING_PAYMENT - 未选择 <500ms
Phase 3a: 支付前试算 实时计算最终金额 不变 不变 - 查询并计算抵扣 100-200ms
Phase 3b: 确认支付 预扣优惠券/Coin 不变 不变 PENDING coupon=RESERVED
coin: available=50, reserved=50
用户操作
Phase 4: 回调 确认扣减全部资源 available=8(确认) PAID PAID coupon=USED
coin: reserved=0, used=50
<200ms
Phase 5: 履约 供应商出票/出码 不变 COMPLETED 不变 不变 异步
Phase 6: 超时 释放全部资源 available=10(回退+2) CANCELLED - coupon=AVAILABLE
coin: available=100, reserved=0
定时任务

核心设计亮点

  1. 防止超卖:创单时锁定库存(预占机制),其他用户无法下单
  2. 用户体验好:先锁定库存,再慢慢选择支付方式和优惠券
  3. 实时试算:支付页面实时展示最终金额(优惠券+Coin+渠道费)
  4. 价格透明:用户清楚看到每一项优惠的明细
  5. 预占-确认机制:库存、优惠券、Coin均采用”预占→确认”两阶段提交
  6. 资源高效:超时未支付自动释放全部资源(15分钟窗口)
  7. 最终一致性:通过 Kafka 事件驱动保证各系统状态一致

4.6 Kafka Topic设计

Topic名称 发布者 订阅者 消息内容 分区数 副本数 保留时间
order.created checkout-service inventory, cart, search, analytics, notification order_id, user_id, items, amount 16 3 24h
payment.paid payment-service order, supplier-gateway, notification, analytics order_id, payment_id, amount, paid_at 16 3 24h
inventory.reserved inventory-service order, analytics sku_id, reserve_id, quantity, expires_at 8 3 24h
inventory.deducted inventory-service analytics, supplier-sync sku_id, quantity, order_id 8 3 24h
order.fulfilled supplier-gateway order, inventory, notification order_id, supplier_order_id, status 16 3 24h
order.cancelled order-service inventory, payment, notification order_id, reason, cancelled_at 8 3 24h
product.updated product-center search, listing, pricing sku_id, update_type, data 8 3 24h
search.indexed search-service analytics sku_ids, indexed_at 4 3 24h
compensation.tasks checkout/order/inventory compensation-worker task_id, type, payload, retry_count 4 3 7d

五、关键技术决策

5.1 数据库设计

分库分表策略(订单表)

  • 分库:8个库(按 user_id % 8
  • 分表:32张表/库(按 order_id % 32
  • 总表数:256张表
  • 路由规则
    1
    2
    3
    db_index = user_id % 8
    table_index = order_id % 32
    table_name = f"order_tab_{table_index}"

订单主表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
CREATE TABLE order_tab (
order_id BIGINT PRIMARY KEY COMMENT '订单ID(雪花算法)',
order_no VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号',
user_id BIGINT NOT NULL COMMENT '用户ID(分库键)',
order_type VARCHAR(20) NOT NULL COMMENT 'flight/hotel/movie/topup',

-- 金额
total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总额',
discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
paid_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额',

-- 快照引用
price_snapshot_id VARCHAR(64) NOT NULL COMMENT '价格快照ID',
product_snapshot_id VARCHAR(64) NOT NULL COMMENT '商品快照ID',
reserve_ids JSON COMMENT '库存预占ID列表',

-- 状态
order_status VARCHAR(20) NOT NULL COMMENT '订单状态',
payment_status VARCHAR(20) NOT NULL COMMENT '支付状态',
fulfillment_status VARCHAR(20) NOT NULL COMMENT '履约状态',

-- 供应商信息
supplier_id BIGINT COMMENT '供应商ID',
supplier_order_id VARCHAR(64) COMMENT '供应商订单ID',

-- 时间戳
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
paid_at TIMESTAMP NULL,
fulfilled_at TIMESTAMP NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

-- 乐观锁
version INT NOT NULL DEFAULT 0 COMMENT '版本号',

INDEX idx_user_id (user_id),
INDEX idx_order_status (order_status, created_at),
INDEX idx_created_at (created_at),
INDEX idx_supplier (supplier_id, supplier_order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';

5.2 缓存架构(三级缓存)

层级 技术 容量 TTL 命中率 延迟 适用场景
L1本地缓存 Ristretto 500MB/实例 5分钟 80%+ <1ms 热点商品、配置
L2分布式缓存 Redis Cluster 1TB 5-30分钟 95%+ <5ms 商品详情、价格、库存
L3数据库 MySQL - - - 10-50ms 权威数据源

Redis数据结构设计

数据类型 Redis结构 Key示例 TTL 用途
商品信息 Hash product:sku:12345 30min 商品详情
库存数据 Hash inventory:sku:12345 5min 库存实时查询
购物车 Hash cart:user:67890 7天 购物车主存储
价格快照 String snapshot:price:uuid-xxx 30min 价格快照
库存预占 String reserve:uuid-yyy 15min 库存预占记录
预占索引 ZSet reserve:expiry - 按过期时间排序
幂等Key String idempotent:checkout:token-zzz 5min 防重复提交
限流计数 String ratelimit:user:67890:checkout 1min 用户限流

5.3 分布式事务(Saga模式)

订单创建事务(跨3个服务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Checkout Service] - Saga协调者

Step 1: 确认库存预占
├─ 调用 Inventory.ConfirmReserve(reserve_ids)
├─ 成功 → 继续
└─ 失败 → 返回错误

Step 2: 扣券
├─ 调用 Marketing.DeductCoupon(coupon_code)
├─ 成功 → 继续
└─ 失败 → 补偿 Step 1(释放库存)

Step 3: 创建订单
├─ 调用 Order.Create(order_data)
├─ 成功 → 返回 order_id
└─ 失败 → 补偿 Step 1+2(释放库存+回退券)

补偿策略:
• 同步补偿:立即回滚(超时1s内)
• 异步补偿:写入Kafka补偿队列,定时任务重试
• 人工介入:3次重试失败,告警+人工处理

5.4 超卖防护

核心品类(机票/酒店)- 零容忍

  1. Redis预占(原子操作)
  2. MySQL权威数据(15分钟后确认)
  3. 供应商实时库存(下单时二次确认)
  4. 定时对账(每5分钟)

长尾品类(充值/礼品卡)- 可补偿

  1. Redis预占
  2. 真实超卖 → 人工补偿(补发券码、赔付现金、推荐替代商品)
  3. 对账周期放宽(每小时)

5.5 幂等性设计

层级 方案 实现
客户端幂等Token 前端生成UUID Redis SET NX去重,5分钟TTL
订单号唯一性 雪花算法 DB唯一索引 UNIQUE KEY (order_id)
支付回调幂等 payment_id作为幂等Key UPDATE order SET status='PAID' WHERE order_id=? AND status='PENDING_PAYMENT'
营销扣减幂等 券扣减记录唯一索引 UNIQUE KEY (coupon_code, order_id) + Redis原子操作

5.6 架构决策记录(ADR)

本节记录系统设计过程中的关键架构决策,包括决策背景、备选方案、最终决策及理由。

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

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

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

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

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

理由

  1. 单一职责原则(SRP)

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 方案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无感知,始终收到完整输入数据
    • 降级逻辑不混入业务计算
  6. 更好的可复用性

    • Pricing Service可被多个场景复用(搜索、详情、结算)
    • 输入参数标准化(base_price + promo_info)
    • 不依赖特定的服务调用链路

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SearchOrchestrator(聚合服务)
func (o *SearchOrchestrator) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
// Step 1: 获取sku_ids(从ES)
// Step 2: 并发调用Product + Inventory
// Step 3: 调用Marketing获取营销信息
promos, err := o.marketingClient.BatchGetPromotions(ctx, skuIDs, req.UserID)
if err != nil {
promos = make(map[int64]*PromoInfo) // 降级:空促销
}

// Step 4: 调用Pricing计算价格(传入营销信息)
priceItems := buildPriceItems(basePriceMap, promos) // 聚合层组装数据
prices, err := o.pricingClient.BatchCalculatePrice(ctx, priceItems)

return &SearchResponse{...}
}

// PricingService(计价中心)- 纯函数,只负责计算
func (s *PricingService) BatchCalculatePrice(ctx context.Context, items []*PriceItem) (map[int64]*Price, error) {
results := make(map[int64]*Price)
for _, item := range items {
finalPrice := item.BasePrice // 基础价格

// 应用促销折扣(数据来自聚合层)
if item.PromoInfo != nil {
finalPrice = finalPrice * item.PromoInfo.DiscountRate
}

results[item.SkuID] = &Price{
OriginalPrice: item.BasePrice,
FinalPrice: finalPrice,
Discount: item.BasePrice - finalPrice,
}
}
return results, nil
}

影响范围

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

后续行动

  • ✓ 已实现:Aggregation Service编排逻辑
  • ✓ 已实现:Pricing Service纯计算逻辑
  • ✓ 已实现:Marketing Service RPC接口

ADR-002: 库存预占时机

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

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

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

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

理由

  1. 减少无效预占

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

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

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

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

权衡

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

降低缺点的措施

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

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 试算接口:只查询库存,不预占
func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
// 查询库存状态(READ)
stocks, _ := s.inventoryClient.BatchCheckStock(ctx, req.SkuIDs)

// 计算价格...

return &CalculateResponse{
CanCheckout: allInStock(stocks, req.Items), // 是否可下单
Items: items,
Total: total,
}, nil
}

// 确认下单接口:预占库存
func (s *CheckoutService) Confirm(ctx context.Context, req *ConfirmRequest) (*ConfirmResponse, error) {
// 预占库存(WRITE)
reserveIDs, err := s.inventoryClient.ReserveStock(ctx, req.Items)
if err != nil {
return nil, fmt.Errorf("库存不足: %w", err)
}

// 创建订单...

return &ConfirmResponse{OrderID: orderID}, nil
}

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. 代码复用

    1
    2
    3
    4
    5
    6
    7
    8
    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返回标准数据格式,前端各端按需裁剪

权衡

  • ✓ 优点:减少重复代码、易于维护、业务逻辑统一
  • ✗ 缺点:无法深度定制端特性(如App性能优化)

适用场景

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

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

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

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

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

库存管理类型(ManagementType)

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

库存单位类型(UnitType)

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

理由

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

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 库存策略接口
type StockStrategy interface {
Reserve(ctx context.Context, req *ReserveRequest) (*ReserveResponse, error)
Deduct(ctx context.Context, req *DeductRequest) error
Release(ctx context.Context, reserveID string) error
}

// 实时库存策略(机票、酒店)
type RealtimeStockStrategy struct {
supplierClient rpc.SupplierClient
}

func (s *RealtimeStockStrategy) Reserve(ctx context.Context, req *ReserveRequest) (*ReserveResponse, error) {
// 调用供应商实时预占接口
return s.supplierClient.ReserveStock(ctx, req.SkuID, req.Quantity)
}

// 池化库存策略(充值卡、优惠券)
type PooledStockStrategy struct {
redis redis.Client
}

func (s *PooledStockStrategy) Reserve(ctx context.Context, req *ReserveRequest) (*ReserveResponse, error) {
// Redis原子扣减
script := `
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
else
return 0
end
`
return s.redis.Eval(ctx, script, []string{key}, req.Quantity).Result()
}

影响范围

  • Inventory Service:实现多种库存策略
  • Supplier Gateway:对接供应商实时库存接口
  • Product Center:商品配置中标记ManagementType

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-006: 为什么引入聚合服务

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

问题描述:为什么不直接在API Gateway调用多个微服务,而是引入聚合服务?

决策:引入Aggregation Service作为编排层。

理由

  1. API Gateway职责单一

    • API Gateway只负责:鉴权、限流、路由、协议转换
    • 不应包含业务编排逻辑(违反SRP原则)
  2. 复杂编排逻辑

    • 搜索场景:5步调用(ES → Product → Inventory → Marketing → Pricing)
    • 详情场景:6步调用(Product → 4个并发 → Pricing)
    • 结算场景:6步调用(Product → Inventory → Marketing → Pricing)
    • 如果放在Gateway,会导致Gateway代码膨胀
  3. 数据依赖编排

    • 有些调用必须串行(Pricing依赖Marketing结果)
    • 有些调用可以并发(Product + Inventory)
    • 需要专门的编排层处理复杂依赖关系
  4. 统一降级策略

    • 聚合层可以根据业务场景灵活降级
    • 搜索场景:Marketing失败可降级(只展示base_price)
    • 详情场景:Marketing失败不降级(必须返回错误)
  5. 性能优化空间

    • 聚合层可以统一缓存聚合结果
    • 支持批量调用优化(BatchGet)
    • 支持超时控制和熔断

架构对比

1
2
3
4
5
6
7
8
9
10
11
方案1(无聚合层):
API Gateway → Product, Inventory, Marketing, Pricing(直接调用)
├─ Gateway需要处理复杂编排
├─ Gateway需要处理数据依赖
└─ Gateway代码膨胀,违反SRP

方案2(有聚合层):✓
API Gateway → Aggregation → Product, Inventory, Marketing, Pricing
├─ Gateway职责单一(鉴权、限流、路由)
├─ Aggregation专注编排(数据获取、降级、聚合)
└─ 微服务职责单一(各司其职)

影响范围

  • 新增服务:Aggregation Service
  • QPS估算:6000(正常)/ 30000(大促)
  • 部署规模:6副本(正常)/ 18副本(大促)

ADR-007: MySQL分库分表策略

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

问题描述:订单表、商品表数据量大(千万级),如何分库分表?

决策

  • 订单表:按user_id分库(8库),按create_time分表(64表)
  • 商品表:按category_id分库(4库),不分表

理由

订单表分库策略

  1. user_id分库:

    • ✓ 用户维度查询最频繁(”我的订单”)
    • ✓ 避免跨库查询,性能最优
    • ✗ 但按订单号查询会跨库(通过路由表解决)
  2. create_time分表:

    • ✓ 历史订单按月归档(避免单表过大)
    • ✓ 查询”我的订单”时按时间范围查询(只查最近几个月)

商品表分库策略

  1. category_id分库:

    • ✓ 分类浏览场景性能最优
    • ✓ 不同品类的供应商同步逻辑隔离
    • ✗ 跨品类搜索需要聚合(通过ES解决)
  2. 不分表:

    • 商品数据量可控(百万级),单表足够
    • 避免过度设计

路由表设计

1
2
3
4
5
6
7
8
-- 订单路由表(解决按order_id查询的跨库问题)
CREATE TABLE order_route (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
db_index TINYINT NOT NULL, -- 库索引(0-7)
table_index TINYINT NOT NULL, -- 表索引(0-63)
INDEX idx_user_id (user_id)
) ENGINE=InnoDB;

查询示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 按order_id查询(需要路由表)
func (r *OrderRepo) GetByOrderID(orderID int64) (*Order, error) {
// Step 1: 查询路由表
route, err := r.routeTable.GetRoute(orderID)
if err != nil {
return nil, err
}

// Step 2: 根据路由信息查询目标分片
db := r.shards[route.DBIndex]
tableName := fmt.Sprintf("order_%d", route.TableIndex)

return db.QueryOne("SELECT * FROM ? WHERE order_id = ?", tableName, orderID)
}

// 按user_id查询(不需要路由表)
func (r *OrderRepo) GetByUserID(userID int64, limit int) ([]*Order, error) {
dbIndex := userID % 8 // 直接计算库索引
db := r.shards[dbIndex]

// 查询最近3个月的订单(最多3张表)
tables := r.getRecentTables(3) // ["order_62", "order_63", "order_0"]

// 并发查询3张表,合并结果
return db.QueryMultipleTables(tables, "SELECT * FROM ? WHERE user_id = ? ORDER BY create_time DESC LIMIT ?", userID, limit)
}

ADR-008: 试算接口是否复用详情页缓存数据

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

问题描述:用户从商品详情页点击”结算”进入试算接口,能否复用详情页已缓存的商品信息(尤其是营销数据)来减少后端调用,还是需要重新查询Marketing Service?

决策:采用”快照ID(snapshot_id)+ 最终创单二次校验”方案

核心设计思想

1
2
试算阶段:允许使用快照数据(性能优先)
创单阶段:强制实时校验(准确性优先)

用户行为路径

1
详情页(生成快照)→ 点击结算(通常30秒内)→ 试算(使用快照)→ 确认下单(实时校验)

三种方案对比

方案 实现方式 响应时间 数据准确性 后端压力 推荐
A. 完全复用(无快照) 前端缓存→直接使用 50ms ❌ 低(营销可能变化) ✓ 最低
B. 快照ID + 过期判断 快照未过期→使用缓存
快照过期→重新查询
80ms ✓ 高(最终创单二次校验) ✓ 低
C. 完全不复用 试算也重新查询所有服务 230ms ✓ 最高 ❌ 高

理由

1. 各类数据的复用可行性分析

数据类型 变化频率 是否可复用 理由
商品基础信息(title/images) 低(小时级) ✅ 可复用 不常变化,复用安全
基础价格(base_price) 低(天级) ✅ 可复用 价格变化慢,可缓存
营销活动(promotions) 中(分钟级) ⚠️ 条件复用 快照5分钟有效期,创单时二次校验
库存状态(stock) 高(秒级) ❌ 必须实时查 防止超卖,不能使用缓存

2. 性能优化显著

  • 响应时间降低65%(80ms vs 230ms)
  • Marketing Service QPS降低80%(命中率80%)
  • 用户连续试算3次场景:240ms vs 690ms

3. 数据一致性保证(关键)

  • 试算阶段:允许使用5分钟内的快照数据(性能优先,用户体验好)
  • 创单阶段:强制实时查询+校验营销活动(准确性优先,防止资损)
  • ✅ 快照过期自动降级到重新查询(透明对用户)
  • ✅ 最终创单时的二次校验是最后一道防线(核心安全保障)

实现方案

前端实现(传递快照数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 1. 详情页响应(附带快照ID和过期时间)
GET /products/12345/detail
{
"sku_id": 12345,
"price": {
"base_price": 299.00,
"final_price": 269.00
},
"promotions": {
"active_promotions": [...],
"coupons": [...]
},
"snapshot": {
"snapshot_id": "snap:12345:1744633200", // 快照ID(唯一标识)
"created_at": 1744633200, // 快照创建时间
"expires_at": 1744633500, // 快照过期时间(5分钟后)
"ttl": 300 // 快照有效期(秒)
}
}

// 2. 前端存储详情页数据(包含快照信息)
const detailData = {
sku_id: 12345,
base_price: 299.00,
promotions: {...},
snapshot_id: "snap:12345:1744633200",
snapshot_expires_at: 1744633500
};
localStorage.setItem('product_12345', JSON.stringify(detailData));

// 3. 试算接口(携带快照数据)
POST /checkout/calculate
{
"user_id": 67890,
"items": [
{
"sku_id": 1001,
"quantity": 2,
"snapshot": { // 前端缓存的快照数据
"snapshot_id": "snap:1001:1744633200",
"expires_at": 1744633500,
"data": {
"base_price": 299.00,
"promotions": {...}
}
}
},
{
"sku_id": 1005,
"quantity": 1,
"snapshot": null // 无详情页快照(需要查询)
}
]
}

后端实现(试算接口:使用快照数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// CheckoutService.Calculate - 试算接口(性能优先)
func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
var (
needQuerySKUs []int64 // 需要重新查询的SKU
snapshotData map[int64]*SnapshotData // 可复用的快照数据
)

now := time.Now().Unix()

// Step 1: 判断每个SKU的快照是否过期
for _, item := range req.Items {
if item.Snapshot != nil {
// 快照未过期,直接使用
if item.Snapshot.ExpiresAt > now {
snapshotData[item.SkuID] = item.Snapshot.Data
continue
}
// 快照已过期,需要重新查询
}

// 无快照或快照过期,需要查询
needQuerySKUs = append(needQuerySKUs, item.SkuID)
}

// Step 2: 只查询未命中快照的SKU
var products []*Product
var promos []*Promotion
if len(needQuerySKUs) > 0 {
// 并发调用Product + Marketing
products, _ = s.productClient.BatchGetProducts(ctx, needQuerySKUs)
promos, _ = s.marketingClient.BatchGetPromotions(ctx, needQuerySKUs, req.UserID)
}

// Step 3: 合并快照数据和查询数据
allProducts := s.mergeSnapshotAndQueried(snapshotData, products)

// Step 4: 库存必须实时查询(不能使用快照)
allSKUs := extractAllSKUs(req.Items)
stocks, _ := s.inventoryClient.BatchCheckStock(ctx, allSKUs)

// Step 5: 调用Pricing计算价格
prices, _ := s.pricingClient.BatchCalculatePrice(ctx, allProducts)

return &CalculateResponse{
Items: buildCalculateItems(allProducts, prices, stocks),
TotalPrice: calculateTotal(prices),
SnapshotUsage: buildSnapshotUsageReport(snapshotData, needQuerySKUs), // 快照使用情况
}, nil
}

// 合并快照数据和查询数据
func (s *CheckoutService) mergeSnapshotAndQueried(
snapshotData map[int64]*SnapshotData,
queriedProducts []*Product,
) []*ProductData {
merged := make([]*ProductData, 0)

// 添加快照数据
for skuID, snapshot := range snapshotData {
merged = append(merged, &ProductData{
SkuID: skuID,
BasePrice: snapshot.BasePrice,
Promotions: snapshot.Promotions,
Source: "snapshot", // 标记数据来源
})
}

// 添加查询数据
for _, product := range queriedProducts {
merged = append(merged, &ProductData{
SkuID: product.SkuID,
BasePrice: product.BasePrice,
Promotions: product.Promotions,
Source: "realtime", // 标记数据来源
})
}

return merged
}

创单时的商品快照策略(重点说明)

核心原则前端传递快照 + 后端强制实时查询 + 创单后保存快照

完整流程说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
┌─────────────────────────────────────────────────────────────────┐
│ 阶段1: 用户在详情页/购物车看到的信息(前端快照) │
├─────────────────────────────────────────────────────────────────┤
│ 前端展示: │
│ 商品名称:iPhone 15 Pro 256GB 深空黑色 │
│ 价格:¥7999(可能是5分钟前的快照) │
│ 库存:有货 │
│ │
│ 前端记录: │
│ expected_price = 7999元 │
│ snapshot_id = "snap_20260415_143022" │
│ snapshot_expires_at = 1713168622(5分钟后过期) │
└─────────────────────────────────────────────────────────────────┘

用户点击"提交订单"

┌─────────────────────────────────────────────────────────────────┐
│ 阶段2: 后端创单时的数据获取(强制实时查询) │
├─────────────────────────────────────────────────────────────────┤
│ ❌ 不使用前端传递的快照数据 │
│ ✅ 强制调用 ProductService.GetProducts() 获取最新数据 │
│ ✅ 强制调用 MarketingService.GetPromotions() 获取最新活动 │
│ ✅ 强制调用 PricingService.Calculate() 重新计算价格 │
│ │
│ 实时查询结果: │
│ 商品名称:iPhone 15 Pro 256GB 深空黑色 │
│ 基础价格:¥8999(商家涨价了!) │
│ 活动折扣:满8000减500(新活动) │
│ 实际价格:¥8499 │
│ │
│ 价格对比: │
│ expected_price = 7999 │
│ actual_price = 8499 │
│ diff = 500(差异 > 阈值100元) │
│ → 返回错误,提示用户价格已变化 │
└─────────────────────────────────────────────────────────────────┘

价格校验通过

┌─────────────────────────────────────────────────────────────────┐
│ 阶段3: 创建订单时保存商品快照(防止后续变更) │
├─────────────────────────────────────────────────────────────────┤
│ 生成商品快照(JSON格式): │
│ { │
│ "snapshot_id": "order_snap_1001_20260415_143100", │
│ "snapshot_time": 1713168660, │
│ "items": [ │
│ { │
│ "item_id": 50001, │
│ "sku_id": 500011, │
│ "item_name": "iPhone 15 Pro 256GB 深空黑色", │
│ "base_price": 8999, // 创单时的实际价格 │
│ "actual_price": 8499, // 折后价 │
│ "quantity": 1, │
│ "shop_id": 10001, │
│ "shop_name": "Apple官方旗舰店", │
│ "category_id": 1001, │
│ "attributes": {"颜色": "深空黑色", "容量": "256GB"} │
│ } │
│ ], │
│ "promotions": [ │
│ { │
│ "promotion_id": 20001, │
│ "promotion_name": "满8000减500", │
│ "discount_amount": 500 │
│ } │
│ ] │
│ } │
│ │
│ 保存到订单表: │
│ INSERT INTO orders ( │
│ order_id, user_id, shop_id, │
│ product_snapshot, -- 完整快照JSON │
│ total_amount, │
│ ... │
│ ) VALUES (...) │
└─────────────────────────────────────────────────────────────────┘
为什么这样设计?
问题 解决方案 原因
前端价格可能被篡改 后端强制重新查询和计算 防止用户修改请求参数,恶意降价下单
商品价格/活动可能变化 实时查询最新数据 避免用户用过期价格下单,造成资损
创单后商品可能下架/改价 保存创单时的商品快照 确保订单详情永久可查,售后有据可依
用户体验:价格突变 前后端价格对比 + 差异提示 价格变化较大时,提示用户重新确认
三个关键数据源对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1️⃣ 前端传递的快照(仅用于校验和用户体验)
type FrontendSnapshot struct {
SnapshotID string `json:"snapshot_id"` // 前端快照ID
ExpectedPrice int64 `json:"expected_price"` // 用户看到的价格
ExpiresAt int64 `json:"expires_at"` // 快照过期时间
Items []Item `json:"items"` // 商品列表(不可信!)
}

// 2️⃣ 后端实时查询的数据(创单的依据,最高优先级)
type RealtimeData struct {
Products []Product // 从 Product Service 实时查询
Promotions []Promotion // 从 Marketing Service 实时查询
ActualPrice int64 // 从 Pricing Service 实时计算
}

// 3️⃣ 创单后保存的快照(用于订单详情展示和售后)
type OrderSnapshot struct {
SnapshotID string `json:"snapshot_id"`
SnapshotTime int64 `json:"snapshot_time"`
Items []Item `json:"items"` // 创单时的商品信息
Promotions []Promo `json:"promotions"` // 创单时的活动信息
PriceBreakdown Breakdown `json:"breakdown"` // 价格明细
}
价格校验逻辑(防止用户感知差)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 价格对比规则
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, 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),
}
}

后端实现(确认下单接口:强制实时校验)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// OrderService.CreateOrder - 确认下单接口(准确性优先)
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, 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: 预占库存(CAS操作,防止超卖)
reserved, err := s.inventoryClient.ReserveStock(ctx, req.Items)
if err != nil {
return nil, fmt.Errorf("reserve stock failed: %w", err)
}

// Step 5: 实时计算价格(基于最新营销数据)
price, err := s.pricingClient.CalculateFinalPrice(ctx, products, promos)
if err != nil {
// 回滚库存
s.inventoryClient.ReleaseStock(ctx, reserved)
return nil, fmt.Errorf("calculate price failed: %w", err)
}

// Step 5.5: 价格校验(对比前端传递的期望价格)
if req.ExpectedPrice > 0 {
if err := s.validatePriceChange(req.ExpectedPrice, price.FinalPrice); err != nil {
// 价格变化过大,释放库存,返回错误
s.inventoryClient.ReleaseStock(ctx, reserved)
return nil, err
}
}

// Step 6: 生成商品快照(基于实时查询的数据)
snapshot := s.generateProductSnapshot(products, promos, price)
snapshotJSON, _ := json.Marshal(snapshot)

// Step 7: 创建订单(保存快照)
order := &Order{
OrderID: s.generateOrderID(),
UserID: req.UserID,
ShopID: req.ShopID,
Items: req.Items,
TotalPrice: price.FinalPrice,
DiscountAmount: price.DiscountAmount,
PayableAmount: price.PayableAmount,
ProductSnapshot: string(snapshotJSON), // 💾 保存商品快照
Status: OrderStatusPending,
CreateTime: time.Now().Unix(),
ExpireTime: time.Now().Add(15 * time.Minute).Unix(),
}

if err := s.orderRepo.Create(ctx, order); err != nil {
// 回滚库存
s.inventoryClient.ReleaseStock(ctx, reserved)
return nil, fmt.Errorf("create order failed: %w", err)
}

return &CreateOrderResponse{
OrderID: order.OrderID,
TotalPrice: price.FinalPrice,
}, nil
}

// 商品快照数据结构
type ProductSnapshot struct {
SnapshotID string `json:"snapshot_id"`
SnapshotTime int64 `json:"snapshot_time"`
Items []SnapshotItem `json:"items"`
Promotions []SnapshotPromotion `json:"promotions"`
PriceBreakdown PriceBreakdown `json:"price_breakdown"`
}

type SnapshotItem struct {
ItemID int64 `json:"item_id"`
SKUID int64 `json:"sku_id"`
ItemName string `json:"item_name"`
BasePrice int64 `json:"base_price"` // 创单时的基础价格
ActualPrice int64 `json:"actual_price"` // 创单时的实付价格
Quantity int32 `json:"quantity"`
ShopID int64 `json:"shop_id"`
ShopName string `json:"shop_name"`
CategoryID int64 `json:"category_id"`
Attributes string `json:"attributes"` // JSON: {"颜色":"黑色","尺码":"XL"}
}

type SnapshotPromotion struct {
PromotionID int64 `json:"promotion_id"`
PromotionName string `json:"promotion_name"`
PromotionType int32 `json:"promotion_type"`
DiscountAmount int64 `json:"discount_amount"`
}

type PriceBreakdown struct {
TotalAmount int64 `json:"total_amount"` // 商品总价
DiscountAmount int64 `json:"discount_amount"` // 总折扣
CouponDiscount int64 `json:"coupon_discount"` // 优惠券折扣
PromotionDiscount int64 `json:"promotion_discount"` // 活动折扣
PayableAmount int64 `json:"payable_amount"` // 应付金额
}

// 生成商品快照(基于实时查询的数据)
func (s *OrderService) generateProductSnapshot(
products []*Product,
promos []*Promotion,
price *PriceResult,
) *ProductSnapshot {
snapshot := &ProductSnapshot{
SnapshotID: fmt.Sprintf("order_snap_%d_%d", time.Now().Unix(), rand.Int63()),
SnapshotTime: time.Now().Unix(),
Items: make([]SnapshotItem, 0, len(products)),
Promotions: make([]SnapshotPromotion, 0, len(promos)),
}

// 保存商品信息快照
for _, p := range products {
snapshot.Items = append(snapshot.Items, SnapshotItem{
ItemID: p.ItemID,
SKUID: p.SKUID,
ItemName: p.ItemName,
BasePrice: p.BasePrice,
ActualPrice: p.ActualPrice,
Quantity: p.Quantity,
ShopID: p.ShopID,
ShopName: p.ShopName,
CategoryID: p.CategoryID,
Attributes: p.Attributes, // {"颜色": "黑色", "尺码": "XL"}
})
}

// 保存营销活动快照
for _, promo := range promos {
snapshot.Promotions = append(snapshot.Promotions, SnapshotPromotion{
PromotionID: promo.PromotionID,
PromotionName: promo.PromotionName,
PromotionType: promo.PromotionType,
DiscountAmount: promo.DiscountAmount,
})
}

// 保存价格明细快照
snapshot.PriceBreakdown = PriceBreakdown{
TotalAmount: price.TotalAmount,
DiscountAmount: price.DiscountAmount,
CouponDiscount: price.CouponDiscount,
PromotionDiscount: price.PromotionDiscount,
PayableAmount: price.PayableAmount,
}

return snapshot
}

// 校验营销活动有效性
func (s *OrderService) validatePromotion(promo *Promotion) bool {
now := time.Now()

// 1. 检查时间范围
if now.Before(promo.StartTime) || now.After(promo.EndTime) {
return false
}

// 2. 检查库存(如果是限量活动)
if promo.StockLimit > 0 && promo.StockRemaining <= 0 {
return false
}

// 3. 检查用户参与次数(如果有限制)
if promo.UserLimit > 0 && promo.UserUsedCount >= promo.UserLimit {
return false
}

return true
}

快照机制设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
快照ID生成规则:
snapshot_id = "snap:{sku_id}:{timestamp}"
expires_at = created_at + 300(5分钟)

快照特性:
1. 无需后端存储(前端自带快照数据)
2. 简单过期判断(expires_at > now)
3. 快照过期自动降级到实时查询
4. 创单时强制实时校验(不使用快照)

快照生命周期:
1. 详情页访问 → 生成快照(Aggregation Service)
2. 点击结算 → 携带快照(前端传递)
3. 试算计算 → 判断过期(Checkout Service)
- 未过期:直接使用快照数据
- 已过期:重新查询Marketing Service
4. 确认下单 → 强制实时校验(Order Service)
- 重新查询所有营销活动
- 校验活动有效性(时间、库存、用户限制)

商品快照策略总结表

数据源 使用场景 可信程度 主要作用 生成/保存时机
① 前端传递的快照 试算接口 ⚠️ 不可信 • 性能优化(减少RPC)
• 价格对比基准
详情页/购物车生成,试算时传递
② 后端实时查询 创单接口 ✅ 完全可信 • 防止价格篡改
• 获取最新数据
创单时强制查询
③ 订单保存的快照 订单详情/售后 ✅ 历史准确 • 永久展示订单详情
• 售后纠纷凭证
创单成功后保存到订单表

关键原则

1
2
3
4
5
6
┌────────────────────────────────────────────────────────┐
│ 试算阶段:性能优先 → 可用快照(5分钟缓存) │
│ 创单阶段:准确性优先 → 强制实时查询 │
│ 历史查询:可追溯性 → 保存快照到订单表 │
└────────────────────────────────────────────────────────┘
- 防止使用过期/失效的营销活动

试算 vs 创单的数据要求对比(关键)

维度 试算阶段(Calculate) 创单阶段(CreateOrder)
目的 性能优先,快速预览价格 准确性优先,防止资损
商品数据 可使用快照(5分钟) ✅ 强制实时查询
营销数据 可使用快照(5分钟) ✅ 强制实时查询 + 活动有效性校验
库存数据 ✅ 必须实时查询 ✅ 必须实时查询 + CAS预占
价格计算 基于快照/实时混合数据 ✅ 基于最新实时数据
数据一致性 最终一致性(允许5分钟延迟) 强一致性(实时校验)
性能要求 P95 < 300ms P95 < 500ms(可接受稍慢)
安全保障 无资损风险(仅展示) ✅ 多重校验(防止资损)

关键设计原则

  • ✅ 试算允许使用快照(提升性能,用户体验好)
  • ✅ 创单强制实时校验(保证准确性,防止资损)
  • ✅ 快照过期自动降级(透明对用户)
  • ✅ 最终创单是最后一道防线(即使试算用了过期快照,创单也会拦截)

性能提升数据

场景1:用户在详情页停留30秒后点击结算(快照未过期)

指标 方案A(完全复用) 方案B(快照ID)✓ 方案C(不复用)
查询服务数 0个 1个(Inventory) 3个(Product+Marketing+Inventory)
响应时间 50ms 80ms 230ms
快照命中率 100%(风险高) 90%(安全) 0%
数据准确性 ❌ 无保障 ✅ 创单时二次校验 ✅ 实时数据

场景2:用户在详情页停留10分钟后点击结算(快照已过期)

指标 方案B(快照ID)✓ 方案C(不复用)
查询服务数 2个(Product+Marketing) 3个(Product+Marketing+Inventory)
响应时间 180ms 230ms
快照命中率 0%(自动降级) 0%

场景3:用户连续试算3次(调整数量、优惠券,快照未过期)

指标 方案A 方案B(快照ID)✓ 方案C
总查询次数 0次 3次(只查Inventory) 9次
总响应时间 150ms 240ms 690ms
Marketing QPS 0 0(使用快照) 3

权衡

维度 优势 劣势
性能 ✓ 快照命中时响应快65%(80ms vs 230ms)
✓ Marketing Service QPS降低90%
⚠️ 快照过期时需重查(约10%场景)
一致性 ✓ 最终创单强制实时校验(零资损风险)
✓ 快照过期自动降级(透明)
⚠️ 试算阶段允许5分钟延迟(可接受)
复杂度 ✓ 实现简单(无需Redis token)
✓ 前端存储快照,后端仅判断过期
⚠️ 需要前后端配合传递快照数据
安全性 ✓ 创单时多重校验(活动有效性+库存)
✓ 试算用快照不影响最终准确性
✅ 无安全风险(创单是最后防线)

核心优势(相比cache_token方案)

  1. 无需后端存储:快照数据由前端携带,后端无需维护Redis token
  2. 实现更简单:仅需判断expires_at > now,无需复杂的token验证
  3. 营销活动变更无影响:无需监听营销变更事件更新token
  4. 最终创单二次校验:即使快照数据错误,创单阶段也会拦截

影响范围

  • Aggregation Service:商品详情接口返回snapshot字段(snapshot_id + expires_at + data)
  • Checkout Service
    • Calculate接口:判断快照是否过期,未过期则使用快照数据
    • CreateOrder接口:强制实时查询,不使用快照
  • Order Service:确认下单时实时查询+校验营销活动有效性
  • 前端(APP/Web):缓存详情页快照数据,试算时携带snapshot字段

实施建议

  1. 第一阶段:试算接口支持快照数据(快照过期降级到实时查询)
  2. 第二阶段:优化快照TTL(根据用户行为分析,可能调整为3-10分钟)
  3. 第三阶段:增加快照命中率监控,优化用户体验

监控指标

  • 快照命中率(目标90%,即90%用户在5分钟内点击结算)
  • 试算接口P99响应时间(目标<300ms)
  • 创单接口营销校验失败率(目标<1%,即99%的试算价格与创单价格一致)
  • Marketing Service QPS(目标降低90%)

关键设计亮点

“试算用快照(性能优先),创单强制校验(准确性优先)”

这是一个典型的”防御性设计”:即使试算阶段使用了过期快照,最终创单时的实时校验会拦截所有不一致的情况,用户最终支付的价格一定是准确的。


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

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

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

备选方案

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

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

决策理由

  1. 安全性优先于性能

    1
    2
    3
    4
    5
    6
    7
    风险分析:
    - 如果用快照,活动结束但快照未更新 → 用户用秒杀价下单 → 资损
    - 如果用快照,用户篡改价格 → 恶意低价下单 → 资损
    - 性能损失:100-200ms
    - 资损风险:每单可能损失数百至数千元

    结论:100ms的性能代价 << 资损风险
  2. 涉及资金的操作必须实时校验

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    场景:黑产抓包修改快照数据
    快照:{"expected_payable": 799900} // 原价 ¥7,999
    篡改:{"expected_payable": 1} // 改成 ¥0.01

    如果后端使用快照:
    → 按 ¥0.01 创单 → 公司巨额损失!

    强制实时查询:
    → 后端查到实际价格 ¥7,999
    → 对比快照 ¥0.01 vs 实际 ¥7,999
    → 差异巨大,拒绝创单!
  4. 活动可能随时变化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    10:00  秒杀价 ¥7,999,生成快照
    10:04 秒杀活动提前结束(库存售罄)
    10:05 用户提交订单

    如果用快照:
    → 按 ¥7,999 创单(活动已结束!)
    → 资损

    强制查询:
    → 查到活动已结束,价格 ¥8,999
    → 提示用户价格变化
    → 避免资损

快照的真正作用

快照不是为了加速创单(这是常见误解),而是用于:

  1. 加速试算:用户在结算页频繁修改时,使用快照避免重复查询
  2. 价格对比:创单时对比快照价格和实际价格,发现差异就提示用户

设计原则

1
2
试算阶段:性能优先 → 可以使用快照(用户还没提交,风险低)
创单阶段:安全优先 → 必须实时查询(涉及扣款,风险高)

流程对比

1
2
3
4
5
6
7
8
9
10
11
试算流程(用快照):
前端:携带快照(expected_payable = 7999)
后端:判断快照未过期 → 直接使用快照数据
响应:50ms ⚡

创单流程(不用快照):
前端:携带快照(expected_payable = 7999,仅用于对比)
后端:强制查询商品服务 → 强制查询营销服务 → 重新计算价格
后端:actual_payable = 8999
后端:对比 7999 vs 8999 → 返回"价格已变化"
响应:500ms(慢一点,但准确)

成本收益分析

指标 使用快照 强制查询
响应时间 50ms 500ms
性能提升 +90% -
资损风险 高(活动过期、价格篡改)
用户体验 快,但可能被投诉 稍慢,但价格准确
维护成本 复杂(需要处理快照失效) 简单

结论:创单阶段多花100-200ms查询,换取0资损风险,值得!

实施细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (s *OrderService) CreateOrder(req *CreateOrderRequest) error {
// ❌ 即使快照存在且未过期,也不使用
// ✅ 强制实时查询
products := s.productClient.GetProducts(req.Items) // 实时查询
promotions := s.marketingClient.GetPromotions(req.Items) // 实时查询
actualPrice := s.pricingClient.Calculate(products, promotions)

// ✅ 快照只用于对比
if req.Snapshot != nil {
expectedPrice := req.Snapshot.ExpectedPayable
if actualPrice != expectedPrice {
return &PriceChangedError{...}
}
}

// 创建订单...
}

监控指标

  • 创单价格对比差异率(目标 < 5%)
  • 价格变化被拦截次数(监控活动频繁变化)
  • 创单RT(P99 < 1s)

ADR-010: 创单与支付的时序关系(先创单后支付 vs 创单即支付)

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

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

  1. 创单即支付:用户点击”立即购买”后,先支付,支付成功后再创建订单
  2. 先创单后支付:用户点击”提交订单”后,先创建订单(资源扣减),然后再选择支付方式、优惠券等,最后支付

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

两种模式对比

维度 创单即支付(模式A) 先创单后支付(模式B)✓
用户体验 ⚠️ 需要先选择支付方式才能下单 ✅ 先锁定库存,再慢慢支付
库存扣减时机 支付成功后扣减 创单时预占,支付成功后确认扣减
价格计算时机 支付前一次性计算 创单时计算基础价格,支付时计算支付渠道费
优惠券使用 支付前选择 创单时或支付时选择(更灵活)
订单状态 仅两种:未支付、已支付 三种:待支付、已支付、已完成
超时未支付 不存在(支付后才创单) 需要处理(释放预占库存)
资损风险 ⚠️ 高(库存未锁定,可能超卖) ✅ 低(创单时锁定库存)
复杂度 中(需要处理预占、超时释放)

理由

1. 用户体验更好

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

2. 防止超卖(关键)

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

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

3. 价格计算灵活性

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

实现方案

订单状态机设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────────────────────┐
│ 订单状态流转 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [1] PENDING_PAYMENT(待支付) │
│ ↓ 创建订单时的初始状态 │
│ ↓ 库存已预占(15分钟TTL) │
│ ↓ 优惠券可以在此阶段选择 │
│ ├───→ [超时未支付] → [CANCELLED] │
│ │ ↓ 释放库存 │
│ │ ↓ 回退优惠券 │
│ │ │
│ └───→ [用户支付成功] │
│ ↓ │
│ [2] PAID(已支付) │
│ ↓ 支付成功 │
│ ↓ 库存从"预占"转为"确认扣减" │
│ ↓ 优惠券从"预扣"转为"确认扣减" │
│ ↓ 发起供应商履约 │
│ ├───→ [供应商履约失败] → [REFUNDING] │
│ │ ↓ 发起退款 │
│ │ │
│ └───→ [供应商履约成功] │
│ ↓ │
│ [3] COMPLETED(已完成) │
│ ↓ 供应商出票/出码成功 │
│ ↓ 发送凭证给用户 │
│ │
└─────────────────────────────────────────────────────────────────┘

Phase 2: 确认下单(创建订单,不支付)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// CheckoutService.ConfirmCheckout - 确认下单(不支付)
func (s *CheckoutService) ConfirmCheckout(ctx context.Context, req *ConfirmCheckoutRequest) (*ConfirmCheckoutResponse, error) {
// Step 1: 实时查询商品和营销信息(不使用快照)
products, _ := s.productClient.BatchGetProducts(ctx, req.SkuIDs)
promos, _ := s.marketingClient.BatchGetPromotions(ctx, req.SkuIDs, req.UserID)

// Step 2: 库存预占(Redis Lua原子操作,15分钟TTL)
reserved, err := s.inventoryClient.ReserveStock(ctx, req.Items)
if err != nil {
return nil, fmt.Errorf("库存不足: %w", err)
}

// Step 3: 计算订单金额(基础价格 + 营销优惠,不含支付渠道费)
basePrice, err := s.pricingClient.CalculateBasePrice(ctx, products, promos)
if err != nil {
s.inventoryClient.ReleaseStock(ctx, reserved) // 回滚库存
return nil, fmt.Errorf("价格计算失败: %w", err)
}

// Step 4: 创建订单(状态:PENDING_PAYMENT)
order := &Order{
OrderID: s.generateOrderID(),
UserID: req.UserID,
Items: req.Items,
BasePrice: basePrice.Amount, // 商品总价
DiscountPrice: basePrice.Discount, // 营销优惠
Status: OrderStatusPendingPayment,
PayExpireAt: time.Now().Add(15 * time.Minute), // 15分钟支付窗口
ReserveIDs: reserved, // 库存预占ID
}

if err := s.orderRepo.Create(ctx, order); err != nil {
s.inventoryClient.ReleaseStock(ctx, reserved) // 回滚库存
return nil, fmt.Errorf("创建订单失败: %w", err)
}

// Step 5: 发布 order.created 事件(异步)
s.publishOrderCreatedEvent(order)

return &ConfirmCheckoutResponse{
OrderID: order.OrderID,
BasePrice: basePrice.Amount,
DiscountPrice: basePrice.Discount,
AmountToPay: basePrice.Amount - basePrice.Discount, // 待支付金额(未含支付渠道费)
PayExpireAt: order.PayExpireAt,
}, nil
}

Phase 3: 支付(计算支付渠道费 + 执行支付)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// PaymentService.CreatePayment - 发起支付
func (s *PaymentService) CreatePayment(ctx context.Context, req *CreatePaymentRequest) (*CreatePaymentResponse, error) {
// Step 1: 查询订单(必须是待支付状态)
order, err := s.orderClient.GetOrder(ctx, req.OrderID)
if err != nil {
return nil, err
}
if order.Status != OrderStatusPendingPayment {
return nil, fmt.Errorf("订单状态不正确: %s", order.Status)
}
if time.Now().After(order.PayExpireAt) {
return nil, fmt.Errorf("订单已过期")
}

// Step 2: 选择/校验优惠券(如果用户在支付时选择)
var couponDiscount float64
if req.CouponID != "" {
coupon, err := s.marketingClient.ValidateCoupon(ctx, req.CouponID, req.UserID)
if err != nil {
return nil, fmt.Errorf("优惠券无效: %w", err)
}
couponDiscount = coupon.Amount

// 预扣优惠券(支付成功后确认扣减)
if err := s.marketingClient.ReserveCoupon(ctx, req.CouponID, req.UserID); err != nil {
return nil, fmt.Errorf("优惠券预扣失败: %w", err)
}
}

// Step 3: 计算支付渠道费(手续费、分期费等)
channelFee := s.calculateChannelFee(req.PaymentChannel, order.AmountToPay)

// Step 4: 计算最终支付金额
finalAmount := order.AmountToPay - couponDiscount + channelFee

// Step 5: 创建支付记录
payment := &Payment{
PaymentID: s.generatePaymentID(),
OrderID: req.OrderID,
UserID: req.UserID,
PaymentChannel: req.PaymentChannel,
BaseAmount: order.AmountToPay, // 订单金额
CouponDiscount: couponDiscount, // 优惠券抵扣
ChannelFee: channelFee, // 支付渠道费
FinalAmount: finalAmount, // 最终支付金额
Status: PaymentStatusPending,
}

if err := s.paymentRepo.Create(ctx, payment); err != nil {
// 回滚优惠券
if req.CouponID != "" {
s.marketingClient.ReleaseCoupon(ctx, req.CouponID, req.UserID)
}
return nil, err
}

// Step 6: 调用支付网关(支付宝、微信等)
payURL, err := s.paymentGateway.CreatePay(ctx, payment)
if err != nil {
return nil, err
}

return &CreatePaymentResponse{
PaymentID: payment.PaymentID,
PayURL: payURL,
FinalAmount: finalAmount,
}, nil
}

// 支付回调(支付宝/微信异步通知)
func (s *PaymentService) PaymentCallback(ctx context.Context, req *PaymentCallbackRequest) error {
// Step 1: 幂等性校验
if s.isDuplicate(req.PaymentID) {
return nil // 重复回调,直接返回成功
}

// Step 2: 更新支付状态
payment, _ := s.paymentRepo.GetByID(ctx, req.PaymentID)
payment.Status = PaymentStatusPaid
payment.PaidAt = time.Now()
s.paymentRepo.Update(ctx, payment)

// Step 3: 更新订单状态:PENDING_PAYMENT → PAID
s.orderClient.UpdateOrderStatus(ctx, payment.OrderID, OrderStatusPaid)

// Step 4: 确认扣减库存(从预占转为实际扣减)
s.inventoryClient.ConfirmReserve(ctx, payment.OrderID)

// Step 5: 确认扣减优惠券
if payment.CouponDiscount > 0 {
s.marketingClient.ConfirmCoupon(ctx, payment.CouponID, payment.UserID)
}

// Step 6: 发布 payment.paid 事件
s.publishPaymentPaidEvent(payment)

return nil
}

超时未支付处理(定时任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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. 发布 order.cancelled 事件
j.publishOrderCancelledEvent(order)
}
}

权衡

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

适用场景分析

场景 推荐模式 理由
高并发秒杀商品 ✅ 先创单后支付 防止超卖,库存锁定是硬需求
机票、酒店预订 ✅ 先创单后支付 用户需要时间确认行程、选支付方式
充值、话费 可选创单即支付 无库存限制,支付即充值
虚拟商品(券码) ✅ 先创单后支付 库存有限,需要锁定
线下优惠券 ✅ 先创单后支付 优惠券数量有限,需要锁定

影响范围

  • Order Service:订单状态机增加 PENDING_PAYMENT 状态
  • Inventory Service:增加库存预占(Reserve)和确认扣减(Confirm)接口
  • Marketing Service:增加优惠券预扣(Reserve)和确认扣减(Confirm)接口
  • Payment Service:支付时重新计算优惠券和渠道费
  • 定时任务:新增超时未支付订单扫描任务

监控指标

  • 未支付率(目标<20%):未支付订单数 / 总创建订单数
  • 库存预占浪费率(目标<15%):超时释放库存数 / 总预占库存数
  • 支付超时率(目标<5%):超过15分钟未支付订单数 / 总创建订单数
  • 库存预占成功率(目标>99%):库存预占成功数 / 库存预占请求数

关键设计亮点

“先创单锁定资源,再支付执行扣减”

这是一个典型的”预占-确认”两阶段提交模式(2PC思想),既保证了库存不超卖,又给用户留出了充足的支付选择时间,是电商高并发场景的标准做法。


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

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

问题描述:在用户提交订单时,前端展示的价格(用户期望价格)与后端实时计算的价格可能存在差异。是否需要比对这两个价格?如何处理差异?

典型场景

1
2
3
4
5
6
7
8
用户在结算页看到:应付299元
点击"提交订单"
后端实时计算:实际399元(促销活动已结束)

问题:
• 是否需要比对299元 vs 399元?
• 是否需要用户确认价格变化?
• 还是直接以后端399元为准?

决策:采用比对价格 + 差异确认策略(方案2)

两种方案对比

维度 方案1:不比对,完全以后端为准 方案2:比对价格,差异确认 ✓
安全性 ✅ 高(不信任前端) ✅ 高(最终以后端为准)
实现复杂度 ✅ 简单(无需比对) ⚠️ 中(需要比对逻辑)
用户体验 ❌ 差(价格突变让用户困惑) ✅ 好(价格透明,用户知情)
投诉风险 ❌ 高(用户认为乱扣费) ✅ 低(明确告知价格变化)
转化率 ❌ 低(用户不信任,放弃支付) ✅ 高(用户知情后选择)
可审计性 ❌ 无(无法追溯期望价格) ✅ 强(记录完整价格变化路径)

方案1的问题(真实案例)

1
2
3
4
5
6
7
8
9
10
【某电商平台的用户投诉】:
用户:"我明明看到是299元,为什么支付时变成399元?你们乱扣费!"
客服:"抱歉,促销活动在您下单时已结束,系统自动按原价计算。"
用户:"为什么不提前告诉我?我不买了!退款!"

结果:
• 投诉率:3.2%
• 用户流失率:15%
• 客服成本:高
• 品牌信任度:下降

方案2的优势(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
【头部电商的最佳实践】:
用户在结算页看到:应付299元
点击"提交订单"
系统检测到价格变化,弹窗提示:

┌─────────────────────────────────────┐
│ 价格变化提醒 │
├─────────────────────────────────────┤
│ 原价格:¥299.00 │
│ 当前价格:¥399.00(涨价¥100) │
│ │
│ 变化原因:限时促销活动已结束 │
│ │
│ [ 取消订单 ] [ 确认并支付 ¥399 ] │
└─────────────────────────────────────┘

结果:
• 投诉率:0.3%(降低90%)
• 用户知情后转化率:65%
• 客服成本:低
• 品牌信任度:提升

理由

1. 安全性保障(防篡改):

  • ✅ 最终以后端实时计算为准
  • ✅ 前端传来的期望价格仅作参考
  • ✅ 防止用户篡改前端数据

2. 用户体验优化(价格透明):

  • ✅ 价格变化明确告知用户
  • ✅ 用户知情同意后继续
  • ✅ 给用户选择权(继续或取消)

3. 业务合规(避免投诉):

  • ✅ 避免”价格欺诈”投诉
  • ✅ 符合《消费者权益保护法》
  • ✅ 降低客服成本

4. 可审计性(问题追溯):

  • ✅ 记录用户期望价格
  • ✅ 记录实际价格
  • ✅ 记录价格差异原因
  • ✅ 便于定位系统bug

实现方案

三级价格保护策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────┐
│ Level 1: 精度误差容忍(≤0.01元) │
│ • 允许舍入误差 │
│ • 静默处理,不提示用户 │
│ • 直接创单 │
└─────────────────────────────────────────┘
↓ 差异 > 0.01元
┌─────────────────────────────────────────┐
│ Level 2: 小幅变化记录(0.01-1元) │
│ • 记录日志(审计用) │
│ • 不阻断创单 │
│ • 订单详情页标注"实付与预期有微小差异" │
└─────────────────────────────────────────┘
↓ 差异 > 1元
┌─────────────────────────────────────────┐
│ Level 3: 显著变化拦截(>1元) │
│ • 阻断创单,返回错误 │
│ • 强制用户确认 │
│ • 告知变化原因 │
│ • 允许用户取消或继续 │
└─────────────────────────────────────────┘

后端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
type CreateOrderRequest struct {
UserID int64
Items []*OrderItem
ExpectedPrice float64 // 前端传入的期望价格
PriceChangeConfirmed bool // 用户是否已确认价格变化
}

type PriceChangedError struct {
ExpectedPrice float64
ActualPrice float64
Difference float64
Reason string
Message string
}

func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
// Step 1: 后端实时计算价格(安全保障)
actualPrice, err := s.pricingClient.CalculateFinalPrice(ctx, req.Items, req.Promos)
if err != nil {
return nil, err
}

// Step 2: 比对前端期望价格与后端实际价格
priceDiff := math.Abs(actualPrice.FinalPrice - req.ExpectedPrice)

// Step 3: 三级价格保护策略
const (
acceptableThreshold = 0.01 // Level 1: 允许1分误差(精度/舍入)
warningThreshold = 1.00 // Level 2: 1元以内记录但不阻断
)

if priceDiff > acceptableThreshold {
// Level 2: 小幅变化(0.01-1元)
if priceDiff <= warningThreshold {
// 记录日志但不阻断
s.logger.Info("price changed within warning threshold",
"order_id", generateOrderID(),
"user_id", req.UserID,
"expected_price", req.ExpectedPrice,
"actual_price", actualPrice.FinalPrice,
"difference", priceDiff,
)
} else {
// Level 3: 显著变化(>1元)
if !req.PriceChangeConfirmed {
// 用户未确认,返回错误,要求确认
return nil, &PriceChangedError{
ExpectedPrice: req.ExpectedPrice,
ActualPrice: actualPrice.FinalPrice,
Difference: priceDiff,
Reason: s.explainPriceChange(req.ExpectedPrice, actualPrice),
Message: fmt.Sprintf(
"价格已变化:原%.2f元 → 现%.2f元(%s%.2f元),请确认后继续",
req.ExpectedPrice,
actualPrice.FinalPrice,
getPriceChangeDirection(actualPrice.FinalPrice, req.ExpectedPrice),
priceDiff,
),
}
}

// 用户已确认,记录日志
s.logger.Warn("price changed and user confirmed",
"order_id", generateOrderID(),
"user_id", req.UserID,
"expected_price", req.ExpectedPrice,
"actual_price", actualPrice.FinalPrice,
"difference", priceDiff,
"confirmed", true,
)
}
}

// Step 4: 创建订单(以后端实际价格为准)
order := &Order{
OrderID: generateOrderID(),
UserID: req.UserID,
Items: req.Items,
ExpectedPrice: req.ExpectedPrice, // 记录用户期望价格(审计用)
ActualPrice: actualPrice.FinalPrice, // 实际价格(以此为准)
PriceDiff: priceDiff, // 差异金额(审计用)
PriceChangeConfirmed: req.PriceChangeConfirmed, // 是否已确认
Status: OrderStatusPendingPayment,
CreatedAt: time.Now(),
}

return s.orderRepo.Create(ctx, order)
}

// 解释价格变化原因
func (s *OrderService) explainPriceChange(expectedPrice float64, actualPrice *PriceBreakdown) string {
if actualPrice.FinalPrice < expectedPrice {
return "优惠增加" // 对用户有利,无需详细说明
}

// 分析涨价原因
reasons := []string{}

if len(actualPrice.InvalidPromotions) > 0 {
reasons = append(reasons, "促销活动已结束")
}

if len(actualPrice.InvalidCoupons) > 0 {
reasons = append(reasons, "优惠券已失效")
}

if actualPrice.StockChanged {
reasons = append(reasons, "库存状态变化")
}

if len(reasons) > 0 {
return strings.Join(reasons, "、")
}

return "价格已更新"
}

func getPriceChangeDirection(actualPrice, expectedPrice float64) string {
if actualPrice > expectedPrice {
return "涨价"
}
return "优惠"
}

前端交互流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// Step 1: 首次提交订单
async function submitOrder() {
const expectedPrice = calculateTotalPrice(); // 前端计算的期望价格

try {
const response = await fetch('/orders/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
user_id: userId,
items: cartItems,
expected_price: expectedPrice,
price_change_confirmed: false // 首次提交,未确认
})
});

const data = await response.json();

if (response.ok) {
// 创单成功,跳转支付页
window.location.href = `/payment/${data.order_id}`;
}

} catch (error) {
if (error.code === 'PRICE_CHANGED') {
// 价格变化,显示确认弹窗
showPriceChangeConfirm(error.data);
} else {
alert('下单失败:' + error.message);
}
}
}

// Step 2: 价格变化确认弹窗
function showPriceChangeConfirm(data) {
const isIncrease = data.actual_price > data.expected_price;
const changeType = isIncrease ? '涨价' : '优惠了';
const changeAmount = Math.abs(data.difference).toFixed(2);

const message = `
⚠️ 价格变化提醒

原价格:¥${data.expected_price.toFixed(2)}
当前价格:¥${data.actual_price.toFixed(2)}
${changeType}:¥${changeAmount}

变化原因:${data.reason}

是否继续下单?
`;

if (confirm(message)) {
// 用户确认,重新提交(携带确认标识)
resubmitWithConfirmation(data.actual_price);
} else {
// 用户取消,返回购物车或结算页
history.back();
}
}

// Step 3: 用户确认后重新提交
async function resubmitWithConfirmation(actualPrice) {
try {
const response = await fetch('/orders/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
user_id: userId,
items: cartItems,
expected_price: actualPrice, // 更新为实际价格
price_change_confirmed: true // 标记已确认
})
});

const data = await response.json();

if (response.ok) {
// 创单成功,跳转支付页
window.location.href = `/payment/${data.order_id}`;
}
} catch (error) {
alert('下单失败:' + error.message);
}
}

数据库设计(审计追踪)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE `order` (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,

-- 价格审计字段
expected_price DECIMAL(12, 2) NOT NULL COMMENT '用户期望价格(前端传入)',
actual_price DECIMAL(12, 2) NOT NULL COMMENT '实际价格(后端计算,以此为准)',
price_diff DECIMAL(12, 2) NOT NULL DEFAULT 0 COMMENT '价格差异(actual - expected)',
price_change_confirmed BOOLEAN DEFAULT FALSE COMMENT '用户是否已确认价格变化',
price_change_reason VARCHAR(255) COMMENT '价格变化原因',

status VARCHAR(20) NOT NULL DEFAULT 'PENDING_PAYMENT',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

INDEX idx_user_id (user_id),
INDEX idx_price_diff (price_diff),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB COMMENT='订单表';

监控告警

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 监控价格差异率
func (s *MetricsService) RecordPriceChange(priceDiff float64, orderID int64) {
// 记录差异分布
s.metrics.RecordHistogram("order.price_diff", priceDiff,
map[string]string{
"order_id": fmt.Sprintf("%d", orderID),
})

// 异常告警(差异过大,可能是系统bug)
if priceDiff > 50.0 {
s.alerting.SendUrgentAlert(
"价格差异异常",
fmt.Sprintf("订单%d价格差异%.2f元,超过50元阈值", orderID, priceDiff),
"@pricing-team @sre-oncall",
)
}

// 趋势监控(差异率突增)
dailyAvgDiff := s.getDailyAvgPriceDiff()
if priceDiff > dailyAvgDiff * 3 {
s.alerting.SendAlert(
"价格差异异常",
fmt.Sprintf("订单%d价格差异%.2f元,超过日均值%.2f的3倍",
orderID, priceDiff, dailyAvgDiff),
)
}
}

关键设计亮点

1. 安全与体验的平衡

  • ✅ 最终以后端计算为准(安全)
  • ✅ 价格变化明确告知用户(体验)
  • ✅ 给用户选择权(人性化)

2. 三级分级处理

  • ✅ ≤0.01元:静默处理(精度误差)
  • ✅ 0.01-1元:记录但不阻断(小幅变化)
  • ✅ >1元:强制确认(显著变化)

3. 完整审计链路

  • ✅ 记录期望价格(用户视角)
  • ✅ 记录实际价格(系统计算)
  • ✅ 记录差异原因(便于排查)

4. 监控与告警

  • ✅ 实时监控价格差异分布
  • ✅ 异常告警(差异>50元)
  • ✅ 趋势监控(差异率突增)

与快照机制的关系(ADR-008)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
详情页(Phase 0):
↓ 生成快照,前端存储snapshot_id
用户看到价格:299元(快照数据,5分钟有效)
↓ 用户点击"立即购买"
结算试算(Phase 2):
↓ 使用快照数据(性能优先)
展示价格:299元(可能是快照,可能是实时)
前端记录 expected_price = 299元
↓ 用户点击"提交订单"
创建订单(Phase 3b):
↓ 后端实时计算(不使用快照)
实际价格:399元(促销失效)
↓ 比对:399 vs 299,差异100元
判断:差异>1元 → 阻断创单,要求确认 ✅
↓ 用户确认价格变化
二次提交(confirmed=true):
↓ 以后端实际价格创单
订单价格:399元 ✅
审计记录:expected=299, actual=399, diff=100, reason="促销已结束"

核心设计原则

“试算性能优先,创单准确性优先,价格透明化”

  1. 试算阶段:允许使用快照数据(5分钟有效期),提升性能
  2. 创单阶段:强制实时计算 + 价格比对,保证准确性
  3. 价格变化:明确告知用户,获取知情同意
  4. 最终价格:以后端计算为准,防止篡改
  5. 审计追踪:记录完整价格变化路径

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

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

问题描述:系统中存在两个价格计算场景:试算(Calculate)和创单(CreateOrder)。这两个场景的价格计算逻辑是否应该完全统一?还是应该分别设计?如果有差异,差异在哪里?

核心困惑

1
2
3
4
5
开发疑问:
• 试算和创单都要计算价格,为什么要分两个接口?
• 能不能复用同一套价格计算逻辑?
• 如果复用,为什么还要区分试算和创单?
• 如果不复用,会不会导致试算价格和创单价格不一致?

决策:采用**”统一计算引擎 + 差异化数据来源与校验”**策略


相同点:统一的价格计算框架

两个场景使用完全相同的价格计算引擎,确保计算逻辑一致:

统一部分 说明
同一个Pricing Service 试算和创单都调用同一个微服务
同一个计算函数 PricingService.CalculateFinalPrice(items, promos)
同一套4层架构 基础价格层 → 商品促销层 → 品类促销层 → 订单促销层 → 优惠券层
同一套营销规则 促销优先级、互斥规则、叠加规则完全一致
同一套数据结构 输入:[]*Item, []*Promotion
输出:*PriceDetail
同一套优先级 商品级 > 品类级 > 订单级 > 优惠券

代码示例(统一的计算引擎):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ====== Pricing Service(统一的计算引擎)======
type PricingService struct {}

// 统一的价格计算函数(试算和创单都调用这个)
func (s *PricingService) CalculateFinalPrice(items []*Item, promos []*Promotion) *PriceDetail {
// 1. 计算商品原价
subtotal := calculateSubtotal(items)

// 2. 应用商品级别促销
itemDiscount := applyItemLevelPromotions(items, promos)

// 3. 应用品类级别促销
categoryDiscount := applyCategoryLevelPromotions(items, promos)

// 4. 应用订单级别促销
orderDiscount := applyOrderLevelPromotions(subtotal - itemDiscount - categoryDiscount, promos)

// 5. 应用优惠券
couponDiscount := applyCouponPromotions(subtotal - itemDiscount - categoryDiscount - orderDiscount, promos)

// 6. 返回统一结构
return &PriceDetail{
Subtotal: subtotal,
ItemDiscount: itemDiscount,
CategoryDiscount: categoryDiscount,
OrderDiscount: orderDiscount,
CouponDiscount: couponDiscount,
Total: subtotal - itemDiscount - categoryDiscount - orderDiscount - couponDiscount,
Saved: itemDiscount + categoryDiscount + orderDiscount + couponDiscount,
}
}

为什么要统一计算引擎?

  1. 避免计算结果不一致:如果两套逻辑,可能出现”试算299,创单399”的BUG
  2. 降低维护成本:促销规则修改时只需改一处
  3. 便于测试:只需测试一套计算逻辑
  4. 代码复用:DRY原则(Don’t Repeat Yourself)

不同点:数据来源与校验策略

虽然使用同一个计算引擎,但在数据获取、校验、处理上存在关键差异:

差异维度 试算(Calculate) 创单(CreateOrder)
调用者 Checkout Service Order Service
数据来源 可用快照(ADR-008)
5分钟有效期
强制实时查询(ADR-009)
不使用快照
商品数据 快照 OR 实时查询 ✅ 必须实时查询
营销数据 快照 OR 实时查询 ✅ 必须实时查询
库存查询 只查询,不扣减 必须先预占库存(CAS)
库存依赖 不依赖库存结果 预占失败则拒绝创单
营销校验 基本校验(活动是否存在) 完整校验(有效性+库存+用户资格)
优惠券 只校验有效性 预扣 + 锁定
Coin 只计算可用额度 预扣 + 锁定
计算范围 商品价格 + 营销优惠 商品 + 营销 + 运费 + 服务费
失败处理 降级(移除失效促销,继续计算) 拒绝(返回明确错误,停止创单)
调用频率 高(用户多次修改) 低(一次性操作)
性能目标 P95 < 230ms P95 < 500ms
缓存策略 可缓存(快照命中率80%) 不缓存(强制实时)
幂等性 无需幂等(纯计算) 强幂等(防重复下单)
资损风险 无(仅展示) 高(资源锁定)

架构设计:统一引擎 + 差异化入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// ====== 试算服务(性能优先)======
type CheckoutService struct {
productClient *ProductClient
marketingClient *MarketingClient
inventoryClient *InventoryClient
pricingClient *PricingClient // ← 统一的计算引擎
}

func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
// 【差异1:数据来源 - 可使用快照(ADR-008)】
var products []*Product
var promos []*Promotion

// 判断快照是否过期
if req.Snapshot != nil && !req.Snapshot.IsExpired() {
// 使用快照数据(性能优化)
products = req.Snapshot.Products
promos = req.Snapshot.Promotions
} else {
// 快照过期,重新查询
products, _ = s.productClient.BatchGetProducts(ctx, req.SkuIDs)
promos, _ = s.marketingClient.GetPromotions(ctx, req.SkuIDs, req.UserID)
}

// 【差异2:库存处理 - 只查询,不扣减】
stocks, _ := s.inventoryClient.BatchCheckStock(ctx, req.SkuIDs)

// 【差异3:降级策略 - 允许部分失败】
// 如果营销服务失败,移除失效促销,继续计算
validPromos := filterValidPromotions(promos)

// 【相同:调用统一的计算引擎】✅
priceDetail, _ := s.pricingClient.CalculateFinalPrice(ctx, products, validPromos)

return &CalculateResponse{
Items: products,
PriceDetail: priceDetail,
CanCheckout: checkStock(stocks, req.Items),
}, nil
}

// ====== 创单服务(安全优先)======
type OrderService struct {
productClient *ProductClient
marketingClient *MarketingClient
inventoryClient *InventoryClient
pricingClient *PricingClient // ← 统一的计算引擎(同一个)
}

func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
// 【差异1:数据来源 - 强制实时查询(ADR-009)】
// 注意:即使前端传了快照,也不使用
products, err := s.productClient.BatchGetProducts(ctx, req.SkuIDs)
if err != nil {
return nil, fmt.Errorf("查询商品失败: %w", err)
}

promos, err := s.marketingClient.GetPromotions(ctx, req.SkuIDs, req.UserID)
if err != nil {
return nil, fmt.Errorf("查询营销失败: %w", err)
}

// 【差异2:营销校验 - 完整校验】
for _, promo := range promos {
if !s.validatePromotionStrict(promo) {
return nil, fmt.Errorf("促销活动 %s 已失效", promo.ID)
}
}

// 【差异3:库存预占 - 必须成功】
reservedIDs, err := s.inventoryClient.ReserveStock(ctx, req.Items)
if err != nil {
return nil, fmt.Errorf("库存不足: %w", err)
}
defer func() {
if err != nil {
// 失败时回滚库存
s.inventoryClient.ReleaseStock(ctx, reservedIDs)
}
}()

// 【相同:调用统一的计算引擎】✅
priceDetail, err := s.pricingClient.CalculateFinalPrice(ctx, products, promos)
if err != nil {
return nil, fmt.Errorf("价格计算失败: %w", err)
}

// 【差异4:价格校验 - 严格比对】
if req.ExpectedPrice > 0 {
diff := math.Abs(priceDetail.Total - req.ExpectedPrice)
if diff > 1.0 && !req.PriceChangeConfirmed {
return nil, &PriceChangedError{
Expected: req.ExpectedPrice,
Actual: priceDetail.Total,
}
}
}

// 【差异5:资源扣减】
if err := s.couponClient.ReserveCoupon(ctx, req.CouponCode); err != nil {
return nil, err
}

// 创建订单...
order := &Order{
OrderID: s.generateOrderID(),
UserID: req.UserID,
Items: req.Items,
TotalPrice: priceDetail.Total,
ReservedIDs: reservedIDs,
}

return order, nil
}

为什么不能完全统一?

❌ 方案A:完全统一(试算和创单用同一套逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 假设完全统一
func UnifiedPriceCalculate(items []*Item) *PriceDetail {
// 问题1:应该用快照还是实时查询?
// - 如果用快照 → 创单不安全(ADR-009)
// - 如果实时查询 → 试算性能差

// 问题2:库存应该预占吗?
// - 如果预占 → 试算会锁库存(不合理)
// - 如果不预占 → 创单可能超卖

// 问题3:失败应该降级还是拒绝?
// - 如果降级 → 创单不严格(有资损风险)
// - 如果拒绝 → 试算体验差

return priceDetail
}

结论:场景不同,无法完全统一!


✅ 方案B:分离设计(当前方案)

1
2
3
4
5
试算(Calculate):性能优先,快速反馈

【统一的计算引擎】← 计算逻辑完全一致

创单(CreateOrder):安全优先,准确扣款

关键设计原则

原则1:计算逻辑统一,数据来源差异化

1
2
3
✅ 同一个 PricingService.CalculateFinalPrice()
✅ 试算可用快照(性能优先)
✅ 创单强制实时(安全优先)

原则2:试算追求速度,创单追求准确

1
2
试算:230ms响应,允许使用5分钟内的快照
创单:500ms响应,强制查询最新数据

原则3:试算允许降级,创单必须严格

1
2
试算:营销服务失败 → 移除失效促销,继续计算
创单:营销服务失败 → 返回错误,拒绝创单

原则4:两阶段计算确保用户不被欺骗

1
2
3
Phase 1(试算):用户看到价格 299元
Phase 2(创单):后端重新计算,发现变成 399元
Phase 3(确认):提示用户价格变化,用户确认后继续

与其他ADR的关系

这个ADR是架构决策链的汇总:

1
2
3
4
5
6
7
ADR-008(试算用快照)

ADR-009(创单不用快照)

ADR-012(计算引擎统一,但数据来源和校验差异化)

完整的价格计算决策链

配合使用

  • ADR-008 解决:试算能否用快照?→ 能,5分钟有效期
  • ADR-009 解决:创单能否用快照?→ 不能,强制实时查询
  • ADR-012 解决:两者如何协同?→ 统一引擎,差异化数据

实施建议

1. 代码组织

1
2
3
4
pricing/
├── engine.go # 统一的计算引擎(CalculateFinalPrice)
├── checkout.go # 试算入口(使用快照)
└── order.go # 创单入口(强制实时)

2. 测试策略

1
2
3
✅ 单元测试:测试统一的计算引擎(覆盖所有促销组合)
✅ 集成测试:分别测试试算和创单的完整流程
✅ 一致性测试:确保相同输入下,试算和创单的价格一致

3. 监控指标

1
2
3
4
- 试算P95响应时间(目标 < 230ms)
- 创单P95响应时间(目标 < 500ms)
- 试算与创单价格差异率(目标 < 5%)
- 快照命中率(目标 > 80%)

核心要点总结

“计算引擎统一,数据来源和校验策略差异化”

  1. ✅ 试算和创单使用同一个价格计算引擎
  2. ✅ 试算可用快照数据(性能优先)
  3. ✅ 创单强制实时查询(安全优先)
  4. ✅ 统一引擎确保计算逻辑一致
  5. ✅ 差异化策略满足不同场景需求

ADR-013: 价格在整个交易链路中的流转与计算策略(全局视角)

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

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

核心挑战

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

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


价格流转全局图

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

Hotel场景价格流转示例(完整链路)

以酒店预订为例,展示价格在整个交易链路中的流转细节

💡 可视化图表:参见 /source/diagrams/Excalidraw/hotel-price-flow.excalidraw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
┌────────────────────────────────────────────────────────────────────────┐
│ Hotel预订价格流转全链路 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ 阶段1: Search列表页 (酒店维度最低价) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 用户搜索:上海 2026-05-01 ~ 2026-05-03(2晚) │ │
│ │ ┌────────────┐ │ │
│ │ │ [APP/Web] │ │ │
│ │ └─────┬──────┘ │ │
│ │ ↓ GET /hotel/search │ │
│ │ { │ │
│ │ "city": "上海", │ │
│ │ "check_in": "2026-05-01", │ │
│ │ "check_out": "2026-05-03" │ │
│ │ } │ │
│ │ ↓ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Search Service │─→ Elasticsearch │ │
│ │ └─────────────────┘ │ │
│ │ ↓ │ │
│ │ 返回酒店列表(每个酒店展示最低价): │ │
│ │ [ │ │
│ │ { │ │
│ │ "hotel_id": "H001", │ │
│ │ "hotel_name": "上海和平饭店", │ │
│ │ "lowest_price": 1299.00, // ← 该酒店所有房型的最低价格 │ │
│ │ "room_type": "标准大床房", // ← 最低价对应的房型 │ │
│ │ "promo_label": "限时8折" // ← 营销标签 │ │
│ │ }, │ │
│ │ { │ │
│ │ "hotel_id": "H002", │ │
│ │ "hotel_name": "上海外滩华尔道夫", │ │
│ │ "lowest_price": 2499.00, │ │
│ │ "room_type": "豪华江景房", │ │
│ │ "promo_label": "早鸟优惠" │ │
│ │ } │ │
│ │ ] │ │
│ │ │ │
│ │ 数据来源:ES缓存(异步更新,延迟1-5分钟) │ │
│ │ 性能:P95 < 50ms │ │
│ │ 价格维度:酒店维度最低价(不区分房型细节) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ↓ 用户点击"上海和平饭店" │
│ │
│ 阶段2: Detail详情页 (不同房型 + 营销信息) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 用户进入酒店详情页,查看不同房型 │ │
│ │ ┌────────────┐ │ │
│ │ │ [APP/Web] │ │ │
│ │ └─────┬──────┘ │ │
│ │ ↓ GET /hotel/detail?hotel_id=H001&check_in=2026-05-01 │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Aggregation Service │ │ │
│ │ └──────────┬──────────┘ │ │
│ │ ↓ 并发查询(3个服务) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Hotel Center │ │ Marketing │ │ Inventory │ │ │
│ │ │ (酒店+房型) │ │ Service │ │ Service │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
│ │ ↓ ↓ ↓ │ │
│ │ 房型基础信息 营销活动信息 房间库存 │ │
│ │ ↓ ↓ ↓ │ │
│ │ └──────────────────┴──────────────────┘ │ │
│ │ ↓ │ │
│ │ [Pricing Service] │ │
│ │ ↓ │ │
│ │ 返回详情(不同房型 + 价格 + 营销): │ │
│ │ { │ │
│ │ "hotel_id": "H001", │ │
│ │ "hotel_name": "上海和平饭店", │ │
│ │ "room_types": [ │ │
│ │ { │ │
│ │ "room_type_id": "RT001", │ │
│ │ "room_type": "标准大床房", │ │
│ │ "base_price_per_night": 1599.00, // 单晚基础价 │ │
│ │ "total_nights": 2, │ │
│ │ "base_total": 3198.00, // 2晚原价 │ │
│ │ "promo_price": 2558.40, // 8折后价格 │ │
│ │ "saved": 639.60, │ │
│ │ "promotions": [ │ │
│ │ { │ │
│ │ "id": "P001", │ │
│ │ "type": "限时折扣", │ │
│ │ "desc": "限时8折", │ │
│ │ "discount_rate": 0.8 │ │
│ │ } │ │
│ │ ], │ │
│ │ "available_rooms": 5, // 剩余房间数 │ │
│ │ "breakfast": "含早餐" │ │
│ │ }, │ │
│ │ { │ │
│ │ "room_type_id": "RT002", │ │
│ │ "room_type": "豪华江景房", │ │
│ │ "base_price_per_night": 2199.00, │ │
│ │ "total_nights": 2, │ │
│ │ "base_total": 4398.00, │ │
│ │ "promo_price": 3958.20, // 会员9折 │ │
│ │ "saved": 439.80, │ │
│ │ "promotions": [ │ │
│ │ { │ │
│ │ "id": "P002", │ │
│ │ "type": "会员折扣", │ │
│ │ "desc": "VIP会员9折", │ │
│ │ "discount_rate": 0.9 │ │
│ │ } │ │
│ │ ], │ │
│ │ "available_rooms": 3 │ │
│ │ } │ │
│ │ ], │ │
│ │ "snapshot": { │ │
│ │ "snapshot_id": "snap:H001:1744633200", │ │
│ │ "expires_at": 1744633500, // 5分钟后过期 │ │
│ │ "ttl": 300 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ 数据来源:实时查询 + 生成快照(5分钟) │ │
│ │ 性能:P95 < 200ms │ │
│ │ 价格维度:房型维度(base_price + 营销折扣,个性化) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ↓ 用户选择"标准大床房 x 2间",点击"预订" │
│ │
│ 阶段3: 试算 (考虑数量 + 营销活动) │ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 用户选择:标准大床房 x 2间(2晚) │ │
│ │ ┌────────────┐ │ │
│ │ │ [APP/Web] │ │ │
│ │ └─────┬──────┘ │ │
│ │ ↓ POST /checkout/calculate │ │
│ │ { │ │
│ │ "hotel_id": "H001", │ │
│ │ "items": [ │ │
│ │ { │ │
│ │ "room_type_id": "RT001", │ │
│ │ "quantity": 2, // 2间房 │ │
│ │ "check_in": "2026-05-01", │ │
│ │ "check_out": "2026-05-03", │ │
│ │ "nights": 2, │ │
│ │ "guest_name": "张三", │ │
│ │ "phone": "13800138000" │ │
│ │ } │ │
│ │ ], │ │
│ │ "snapshot": { │ │
│ │ "snapshot_id": "snap:H001:1744633200" // 携带快照 │ │
│ │ } │ │
│ │ } │ │
│ │ ↓ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Checkout Service│ │ │
│ │ └────────┬────────┘ │ │
│ │ ↓ 判断快照是否过期 │ │
│ │ ┌────────┴────────────────────────────┐ │ │
│ │ │ 未过期:使用快照数据(80ms)✨ │ │ │
│ │ │ 已过期:实时查询(230ms) │ │ │
│ │ └────────┬────────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌────────────────┐ │ │
│ │ │ Pricing Service│ │ │
│ │ └────────┬───────┘ │ │
│ │ ↓ │ │
│ │ 返回试算结果: │ │
│ │ { │ │
│ │ "can_checkout": true, │ │
│ │ "items": [ │ │
│ │ { │ │
│ │ "room_type": "标准大床房", │ │
│ │ "quantity": 2, // 2间房 │ │
│ │ "nights": 2, // 2晚 │ │
│ │ "unit_price": 1599.00, // 单间单晚原价 │ │
│ │ "subtotal": 6396.00, // 2间 x 2晚 x 1599 │ │
│ │ "discount": 1279.20, // 8折优惠 │ │
│ │ "final_price": 5116.80 // 优惠后价格 │ │
│ │ } │ │
│ │ ], │ │
│ │ "price_breakdown": { │ │
│ │ "subtotal": 6396.00, // 总原价 │ │
│ │ "room_discount": 1279.20, // 房型优惠(8折) │ │
│ │ "multi_room_discount": 51.17, // 多间房优惠(满2间减1%) │ │
│ │ "total": 5065.63, // 应付总额 │ │
│ │ "saved": 1330.37 │ │
│ │ }, │ │
│ │ "available_coupons": [ // 可用优惠券 │ │
│ │ { │ │
│ │ "code": "HOTEL200", │ │
│ │ "desc": "满5000减200", │ │
│ │ "discount": 200.00 │ │
│ │ } │ │
│ │ ] │ │
│ │ } │ │
│ │ │ │
│ │ 数据来源:快照(80ms)or 实时(230ms) │ │
│ │ 性能:P95 < 230ms(快照命中率80%) │ │
│ │ 价格维度:数量 x 房型 + 营销折扣 + 多间房优惠 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ↓ 用户点击"提交订单" │
│ │
│ 阶段4: 创单 (锁定库存 + 预扣优惠券) │ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ┌────────────┐ │ │
│ │ │ [APP/Web] │ │ │
│ │ └─────┬──────┘ │ │
│ │ ↓ POST /order/create │ │
│ │ { │ │
│ │ "items": [...], │ │
│ │ "expected_price": 5065.63, // 前端期望价格 │ │
│ │ "coupon_codes": ["HOTEL200"], // 选择优惠券 │ │
│ │ "guest_info": { // 入住人信息 │ │
│ │ "name": "张三", │ │
│ │ "phone": "13800138000", │ │
│ │ "id_card": "310101199001011234" │ │
│ │ } │ │
│ │ } │ │
│ │ ↓ │ │
│ │ ┌───────────────┐ │ │
│ │ │ Order Service │ │ │
│ │ └───────┬───────┘ │ │
│ │ ↓ Step 1: 强制实时查询(不使用快照)✅ │ │
│ │ ┌───────┴────────────────────────┐ │ │
│ │ │ [Hotel Center] - 房型信息 │ │ │
│ │ │ [Marketing Service] - 营销校验 │ │ │
│ │ │ [Inventory Service] - 房间库存 │ │ │
│ │ └───────┬────────────────────────┘ │ │
│ │ ↓ Step 2: 预占房间库存(CAS)✅ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ Inventory.ReserveRooms() │ │ │
│ │ │ → 标准大床房 x 2间(2晚) │ │ │
│ │ │ → 返回:reserve_id = "RSV123" │ │ │
│ │ └───────┬───────────────────────┘ │ │
│ │ ↓ Step 3: 预扣优惠券✅ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ Marketing.ReserveCoupon() │ │ │
│ │ │ → HOTEL200(满5000减200) │ │ │
│ │ │ → 返回:coupon_reserve_id │ │ │
│ │ └───────┬───────────────────────┘ │ │
│ │ ↓ Step 4: 实时计算价格✅ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ [Pricing Service] │ │ │
│ │ │ subtotal: 6396.00 │ │ │
│ │ │ - discount: 1279.20 (8折) │ │ │
│ │ │ - multi: 51.17 (多间房) │ │ │
│ │ │ - coupon: 200.00 (券) │ │ │
│ │ │ = actual: 4865.63 │ │ │
│ │ └───────┬───────────────────────┘ │ │
│ │ ↓ Step 5: 价格校验✅ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ expected: 5065.63 │ │ │
│ │ │ actual: 4865.63 │ │ │
│ │ │ diff: 200.00(优惠券生效) │ │ │
│ │ │ → 差异在预期内,继续创单 ✅ │ │ │
│ │ └───────┬───────────────────────┘ │ │
│ │ ↓ Step 6: 创建订单 │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ INSERT INTO orders │ │ │
│ │ │ order_id = "ORD202605..." │ │ │
│ │ │ status = PENDING_PAYMENT │ │ │
│ │ │ total = 4865.63 │ │ │
│ │ │ reserve_id = "RSV123" │ │ │
│ │ │ expires_at = now() + 15min │ │ │
│ │ └───────┬───────────────────────┘ │ │
│ │ ↓ │ │
│ │ 返回订单: │ │
│ │ { │ │
│ │ "order_id": "ORD20260501123456", │ │
│ │ "status": "PENDING_PAYMENT", │ │
│ │ "total": 4865.63, // 最终应付金额 │ │
│ │ "reserved_rooms": 2, // 已预占2间房 │ │
│ │ "expires_at": 1744634100, // 15分钟后过期 │ │
│ │ "price_breakdown": { │ │
│ │ "subtotal": 6396.00, │ │
│ │ "room_discount": 1279.20, │ │
│ │ "multi_room_discount": 51.17, │ │
│ │ "coupon_discount": 200.00, // 优惠券已预扣 │ │
│ │ "total": 4865.63 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ 数据来源:强制实时查询 │ │
│ │ 性能:P95 < 600ms │ │
│ │ 价格维度:房型 + 营销 + 优惠券(已预扣) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ↓ 用户进入支付页,选择Coin、Voucher、支付方式 │
│ │
│ 阶段5: 支付 (Coin + Voucher + 服务费) │ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 用户在支付页选择: │ │
│ │ • 使用100 Coin抵扣(1 Coin = ¥1) │ │
│ │ • 使用Voucher代金券50元 │ │
│ │ • 选择信用卡支付(0.6%手续费) │ │
│ │ ┌────────────┐ │ │
│ │ │ [APP/Web] │ │ │
│ │ └─────┬──────┘ │ │
│ │ ↓ POST /payment/calculate (支付前试算) │ │
│ │ { │ │
│ │ "order_id": "ORD20260501123456", │ │
│ │ "coin_amount": 100, // 使用100 Coin │ │
│ │ "voucher_codes": ["VCH50"], // 50元代金券 │ │
│ │ "payment_method": "credit_card" // 信用卡 │ │
│ │ } │ │
│ │ ↓ │ │
│ │ ┌────────────────┐ │ │
│ │ │ Payment Service│ │ │
│ │ └────────┬───────┘ │ │
│ │ ↓ Step 1: 查询订单金额 │ │
│ │ ┌────────┴────────────────┐ │ │
│ │ │ Order.GetOrder() │ │ │
│ │ │ → total = 4865.63 │ │ │
│ │ └────────┬────────────────┘ │ │
│ │ ↓ Step 2: 校验Coin余额 │ │
│ │ ┌────────┴────────────────┐ │ │
│ │ │ User.GetCoinBalance() │ │ │
│ │ │ → available: 500 Coin │ │ │
│ │ │ → 使用 100 Coin ✅ │ │ │
│ │ └────────┬────────────────┘ │ │
│ │ ↓ Step 3: 校验Voucher │ │
│ │ ┌────────┴────────────────┐ │ │
│ │ │ Marketing.ValidateVoucher│ │ │
│ │ │ → VCH50: 50元有效 ✅ │ │ │
│ │ └────────┬────────────────┘ │ │
│ │ ↓ Step 4: 计算支付渠道费 │ │
│ │ ┌────────┴────────────────┐ │ │
│ │ │ PaymentGateway.GetFee() │ │ │
│ │ │ → 信用卡:0.6%手续费 │ │ │
│ │ └────────┬────────────────┘ │ │
│ │ ↓ Step 5: 计算最终支付金额 │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 订单金额: 4865.63 │ │ │
│ │ │ - Coin: -100.00 │ │ │
│ │ │ - Voucher: -50.00 │ │ │
│ │ │ + 渠道费: +28.59 │ │ │
│ │ │ (4715.63×0.6%) │ │ │
│ │ │ = 最终: 4744.22 │ │ │
│ │ └────────┬────────────────┘ │ │
│ │ ↓ │ │
│ │ 返回试算结果: │ │
│ │ { │ │
│ │ "order_amount": 4865.63, │ │
│ │ "coin_discount": 100.00, │ │
│ │ "voucher_discount": 50.00, │ │
│ │ "payment_fee": 28.59, │ │
│ │ "final_amount": 4744.22, // ← 最终支付金额 │ │
│ │ "breakdown": { │ │
│ │ "room_subtotal": 6396.00, │ │
│ │ "room_discount": 1279.20, │ │
│ │ "multi_room_discount": 51.17, │ │
│ │ "coupon_discount": 200.00, │ │
│ │ "coin_discount": 100.00, │ │
│ │ "voucher_discount": 50.00, │ │
│ │ "payment_fee": 28.59, │ │
│ │ "final": 4744.22 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ ↓ 用户点击"确认支付" │ │
│ │ ↓ POST /payment/create │ │
│ │ ↓ 后端重新计算(防篡改)✅ │ │
│ │ ↓ 预扣Coin和Voucher✅ │ │
│ │ ↓ 创建支付记录 │ │
│ │ ↓ 调用支付网关(支付宝/微信/信用卡) │ │
│ │ │ │
│ │ 数据来源:强制实时(订单+User+Gateway) │ │
│ │ 性能:P95 < 250ms │ │
│ │ 价格维度:订单金额 - Coin - Voucher + 渠道费(最终金额) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ 支付成功后: │
│ • 订单状态:PENDING_PAYMENT → PAID │ │
│ • 房间库存:预占 → 确认占用 │ │
│ • 优惠券:预扣 → 确认消费 │ │
│ • Coin:预扣 → 确认扣减 │ │
│ • Voucher:预扣 → 确认消费 │ │
│ • 发送确认邮件/短信给用户 │ │
└────────────────────────────────────────────────────────────────────────┘

Hotel场景价格存储的特殊性

核心挑战:酒店价格与日期、间夜数、房型高度相关,如何在ES中高效存储和查询?

ES存储策略详解

问题分析

1
2
3
4
5
6
7
酒店价格影响因素:
• 日期:2026-05-01的价格 ≠ 2026-05-02的价格(周末vs工作日)
• 节假日:春节/国庆价格 > 平时价格
• 房型:豪华房 > 标准房
• 间夜数:连住3晚可能有折扣
• 提前预订:提前30天预订(早鸟价)< 当天预订
• 库存:剩余房间数影响价格(最后1间可能涨价)

方案对比

方案 存储内容 优点 缺点 适用场景
方案A:ES存储完整价格日历 每个日期的价格 查询快 数据量大,更新复杂 ❌ 不推荐
方案B:ES只存最低价 酒店维度最低价 简单,性能好 不准确(仅用于排序) ✅ 搜索列表
方案C:混合方案 ES最低价 + Redis价格日历 平衡性能和准确性 需要维护两套数据 ✅ 推荐

推荐方案:分层存储(ES + Redis + MySQL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
┌─────────────────────────────────────────────────────────────┐
│ 酒店价格存储架构(三层存储) │
├─────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: Elasticsearch(搜索列表) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Index: hotel_search │ │
│ │ { │ │
│ │ "hotel_id": "H001", │ │
│ │ "hotel_name": "上海和平饭店", │ │
│ │ "city": "上海", │ │
│ │ "lowest_price": 1299.00, // ← 最低价(用于排序) │ │
│ │ "lowest_room_type": "标准大床房", │ │
│ │ "price_range": { // 价格区间 │ │
│ │ "min": 1299.00, │ │
│ │ "max": 3999.00 │ │
│ │ }, │ │
│ │ "available_date_range": { // 可订日期范围 │ │
│ │ "start": "2026-05-01", │ │
│ │ "end": "2026-12-31" │ │
│ │ }, │ │
│ │ "rating": 4.8, │ │
│ │ "tags": ["五星级", "外滩"] │ │
│ │ } │ │
│ │ │ │
│ │ 更新策略: │ │
│ │ • 每天凌晨3点全量更新最低价 │ │
│ │ • 价格变化>10%时实时更新 │ │
│ │ • 异步更新,延迟1-5分钟可接受 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ Layer 2: Redis(价格日历热数据,详情页+试算) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Key Pattern: hotel:price:{hotel_id}:{room_type_id} │ │
│ │ Data Type: Hash │ │
│ │ │ │
│ │ Key: hotel:price:H001:RT001 │ │
│ │ { │ │
│ │ "2026-05-01": { │ │
│ │ "base_price": 1599.00, // 基础价 │ │
│ │ "weekday_discount": 0.9, // 工作日折扣 │ │
│ │ "available_rooms": 5, // 剩余房间数 │ │
│ │ "min_nights": 1, // 最少入住晚数 │ │
│ │ "max_nights": 30 │ │
│ │ }, │ │
│ │ "2026-05-02": { │ │
│ │ "base_price": 1799.00, // 周五价格涨价 │ │
│ │ "weekend_markup": 1.2, // 周末加价20% │ │
│ │ "available_rooms": 3, │ │
│ │ "min_nights": 2, // 周末最少2晚 │ │
│ │ "max_nights": 30 │ │
│ │ }, │ │
│ │ "2026-05-03": { │ │
│ │ "base_price": 1799.00, │ │
│ │ "available_rooms": 2, │ │
│ │ "min_nights": 1, │ │
│ │ "max_nights": 30 │ │
│ │ } │ │
│ │ // ... 未来90天的价格日历 │ │
│ │ } │ │
│ │ │ │
│ │ 存储策略: │ │
│ │ • 缓存未来90天的价格日历 │ │
│ │ • TTL: 1小时(热数据) │ │
│ │ • 价格变化时实时更新 │ │
│ │ • 库存变化时实时更新 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ Layer 3: MySQL(价格规则和历史数据,源数据) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Table: hotel_price_calendar │ │
│ │ +------------+---------------+-----------+----------+ │ │
│ │ | hotel_id | room_type_id | date | price | │ │
│ │ +------------+---------------+-----------+----------+ │ │
│ │ | H001 | RT001 |2026-05-01 | 1599.00 | │ │
│ │ | H001 | RT001 |2026-05-02 | 1799.00 | │ │
│ │ | H001 | RT001 |2026-05-03 | 1799.00 | │ │
│ │ +------------+---------------+-----------+----------+ │ │
│ │ │ │
│ │ Table: hotel_price_rules(价格规则) │ │
│ │ +------------+----------+----------+---------+-------+ │ │
│ │ | hotel_id | rule_type| weekday | markup |active| │ │
│ │ +------------+----------+----------+---------+-------+ │ │
│ │ | H001 | weekend | Sat,Sun | 1.2 | true | │ │
│ │ | H001 | holiday | 2026CNY | 1.5 | true | │ │
│ │ | H001 | early_bird| 30days | 0.85 | true | │ │
│ │ +------------+----------+----------+---------+-------+ │ │
│ │ │ │
│ │ 存储策略: │ │
│ │ • 存储未来365天的价格日历 │ │
│ │ • 定时任务生成未来价格(基于规则) │ │
│ │ • 运营可手动调整特定日期价格 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

数据流转与更新机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
┌─────────────────────────────────────────────────────────────┐
│ 价格数据流转与同步机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 场景1: 运营修改价格 │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ [运营后台] │ │
│ │ ↓ 修改:H001酒店,2026-05-01,标准房,1599→1399 │ │
│ │ [Price Service] │ │
│ │ ↓ Step 1: 更新MySQL(源数据) │ │
│ │ UPDATE hotel_price_calendar │ │
│ │ SET price = 1399.00 │ │
│ │ WHERE hotel_id='H001' AND date='2026-05-01' │ │
│ │ ↓ Step 2: 发布Kafka事件 │ │
│ │ Topic: hotel.price.changed │ │
│ │ { │ │
│ │ "hotel_id": "H001", │ │
│ │ "room_type_id": "RT001", │ │
│ │ "date": "2026-05-01", │ │
│ │ "old_price": 1599.00, │ │
│ │ "new_price": 1399.00 │ │
│ │ } │ │
│ │ ↓ Step 3: 消费者处理 │ │
│ │ ├─→ [Redis Updater]: 更新价格日历缓存(实时) │ │
│ │ │ HSET hotel:price:H001:RT001 2026-05-01 {...} │ │
│ │ ├─→ [ES Updater]: 判断是否需要更新最低价 │ │
│ │ │ IF new_price < current_lowest_price THEN │ │
│ │ │ UPDATE hotel_search │ │
│ │ │ SET lowest_price = 1399.00 │ │
│ │ └─→ [Notification]: 通知用户(如果有订阅) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 场景2: 用户搜索酒店(列表页) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ [APP/Web] │ │
│ │ ↓ 搜索:上海,2026-05-01 ~ 2026-05-03(2晚) │ │
│ │ [Aggregation Service] │ │
│ │ ↓ Query ES(只用最低价排序,不精确计算) │ │
│ │ GET /hotel_search/_search │ │
│ │ { │ │
│ │ "query": { │ │
│ │ "bool": { │ │
│ │ "filter": [ │ │
│ │ {"term": {"city": "上海"}}, │ │
│ │ {"range": { │ │
│ │ "available_date_range.start": { │ │
│ │ "lte": "2026-05-01" │ │
│ │ } │ │
│ │ }}, │ │
│ │ {"range": { │ │
│ │ "available_date_range.end": { │ │
│ │ "gte": "2026-05-03" │ │
│ │ } │ │
│ │ }} │ │
│ │ ] │ │
│ │ } │ │
│ │ }, │ │
│ │ "sort": [{"lowest_price": "asc"}] // 用最低价排序│ │
│ │ } │ │
│ │ ↓ 返回:酒店列表 + 最低价(仅供参考) │ │
│ │ 注意:这里的价格是"起"价,不是精确价格 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 场景3: 用户点击酒店(详情页) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ [APP/Web] │ │
│ │ ↓ 进入详情:H001,2026-05-01 ~ 2026-05-03(2晚) │ │
│ │ [Aggregation Service] │ │
│ │ ↓ Step 1: 查询Redis价格日历(精确计算) │ │
│ │ HGETALL hotel:price:H001:RT001 │ │
│ │ →获取:2026-05-01, 05-02, 05-03 三天的价格 │ │
│ │ ↓ Step 2: 计算2晚总价 │ │
│ │ 2026-05-01: ¥1599 (工作日) │ │
│ │ 2026-05-02: ¥1799 (周五) │ │
│ │ Total: ¥1599 + ¥1799 = ¥3398 │ │
│ │ ↓ Step 3: 应用营销折扣 │ │
│ │ IF 连住2晚 THEN 9折优惠 │ │
│ │ Final: ¥3398 × 0.9 = ¥3058.20 │ │
│ │ ↓ 返回:精确价格 + 价格明细 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 场景4: 定时任务(价格预生成) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ [Price Generator Job] - 每天凌晨2点执行 │ │
│ │ ↓ Step 1: 基于规则生成未来90天价格 │ │
│ │ FOR each hotel IN all_hotels │ │
│ │ FOR date IN next_90_days │ │
│ │ base_price = get_base_price(hotel, date) │ │
│ │ IF is_weekend(date) THEN │ │
│ │ price = base_price × weekend_markup │ │
│ │ IF is_holiday(date) THEN │ │
│ │ price = base_price × holiday_markup │ │
│ │ INSERT INTO hotel_price_calendar │ │
│ │ ↓ Step 2: 批量更新Redis缓存 │ │
│ │ PIPELINE │ │
│ │ HSET hotel:price:H001:RT001 ... │ │
│ │ HSET hotel:price:H002:RT001 ... │ │
│ │ EXEC │ │
│ │ ↓ Step 3: 更新ES最低价 │ │
│ │ 批量更新所有酒店的lowest_price字段 │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

关键设计要点

1. ES只存”参考价”,不存精确价

1
2
3
4
5
ES中的lowest_price作用:
✅ 用于搜索结果排序
✅ 用于价格区间筛选(¥1000-2000)
✅ 用于展示"¥1299起"的标签
❌ 不用于精确价格计算(因为与日期相关)

2. Redis存储热数据(未来90天)

1
2
3
4
5
6
7
8
9
10
11
为什么选择Redis Hash:
✅ 支持按日期查询(HGET key "2026-05-01")
✅ 支持批量查询多天(HMGET key "05-01" "05-02" "05-03")
✅ 支持原子更新单个日期
✅ 内存占用可控(90天 × 酒店数 × 房型数)

内存估算:
• 单个日期数据:~200 bytes
• 单个房型90天:200B × 90 = 18KB
• 1000家酒店,平均5个房型:1000 × 5 × 18KB = 90MB
• 可接受的内存占用

3. MySQL存储全量数据和规则

1
2
3
4
5
6
7
8
两张关键表:
• hotel_price_calendar: 存储实际价格(365天)
• hotel_price_rules: 存储价格规则(周末加价、节假日加价等)

价格生成逻辑:
1. 基础价格(base_price)
2. 应用规则(weekend_markup, holiday_markup)
3. 运营手动调整(覆盖规则生成的价格)

4. 数据一致性保证

1
2
3
4
5
6
7
8
9
10
11
12
更新顺序:
MySQL → Kafka → Redis → ES
(源数据) (事件) (热数据) (搜索)

一致性策略:
• MySQL: 强一致(源数据)
• Redis: 最终一致(1-5秒延迟)
• ES: 最终一致(1-5分钟延迟,可接受)

容错机制:
• Redis缓存失效 → 降级查询MySQL
• ES数据过期 → 用户看到的是参考价,详情页会更新

实际代码示例

查询价格日历(详情页)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func (s *HotelService) GetPriceCalendar(ctx context.Context, 
hotelID string, roomTypeID string, checkIn, checkOut time.Time) (*PriceDetail, error) {

// Step 1: 生成日期列表
dates := generateDateRange(checkIn, checkOut)

// Step 2: 批量查询Redis
redisKey := fmt.Sprintf("hotel:price:%s:%s", hotelID, roomTypeID)
prices, err := s.redis.HMGet(ctx, redisKey, dates...).Result()

if err != nil || containsNil(prices) {
// Redis缓存失效,降级查询MySQL
return s.getPriceFromMySQL(hotelID, roomTypeID, dates)
}

// Step 3: 计算总价
var totalPrice float64
var priceDetails []*DailyPrice

for i, date := range dates {
dailyPrice := parsePriceJSON(prices[i])
totalPrice += dailyPrice.BasePrice
priceDetails = append(priceDetails, &DailyPrice{
Date: date,
BasePrice: dailyPrice.BasePrice,
Available: dailyPrice.AvailableRooms,
})
}

// Step 4: 应用连住优惠
nights := len(dates)
if nights >= 3 {
totalPrice *= 0.95 // 连住3晚95折
}

return &PriceDetail{
TotalPrice: totalPrice,
Nights: nights,
Daily: priceDetails,
}, nil
}

更新ES最低价(异步任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (s *PriceUpdater) UpdateLowestPriceInES(hotelID string) error {
// Step 1: 查询未来90天所有房型的最低价
lowestPrice, roomType := s.getLowestPriceFromRedis(hotelID)

// Step 2: 更新ES
_, err := s.esClient.Update().
Index("hotel_search").
Id(hotelID).
Doc(map[string]interface{}{
"lowest_price": lowestPrice,
"lowest_room_type": roomType,
"updated_at": time.Now(),
}).
Do(context.Background())

return err
}

Hotel场景的关键特点

  1. Search列表页

    • 展示酒店维度的最低价(不区分房型细节)
    • 数据来源:ES缓存,性能极致(P95 < 50ms)
    • 价格维度:单一(最低价 + 营销标签)
    • 价格说明:显示”¥1299起”,表示该酒店最便宜房型的最低价
  2. Detail详情页

    • 展示不同房型的价格和营销信息
    • 每个房型独立定价(单晚价格 x 入住晚数)
    • 个性化价格(会员价、新人价)
    • 生成快照(5分钟),供后续试算使用
  3. 试算阶段

    • 考虑用户选择的房型数量(多间房)
    • 应用营销活动(限时折扣、会员优惠)
    • 计算多间房优惠(满2间减1%)
    • 预览可用优惠券
  4. 创单阶段

    • 预占房间库存(CAS原子操作,防止超订)
    • 预扣优惠券
    • 强制实时查询,不使用快照
    • 价格校验(对比期望价格)
    • 订单15分钟超时自动取消
  5. 支付阶段

    • 使用Coin抵扣(平台积分)
    • 使用Voucher(代金券)
    • 计算支付渠道费(信用卡手续费0.6%)
    • 最终支付金额 = 订单金额 - Coin - Voucher + 渠道费

标准电商场景对比:iPhone 17价格流转

对比标准电商商品(iPhone 17)与酒店的价格流转差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
┌────────────────────────────────────────────────────────────────────────┐
│ iPhone 17 价格流转(标准电商场景) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ 阶段1: Search列表页 (展示主推SKU价格) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 用户搜索:"iPhone 17" │ │
│ │ ┌────────────┐ │ │
│ │ │ [APP/Web] │ │ │
│ │ └─────┬──────┘ │ │
│ │ ↓ GET /search?keyword=iPhone 17 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Search Service │─→ Elasticsearch │ │
│ │ └─────────────────┘ │ │
│ │ ↓ │ │
│ │ 返回商品列表(SPU+主推SKU): │ │
│ │ [ │ │
│ │ { │ │
│ │ "item_id": "ITEM001", // SPU维度 │ │
│ │ "title": "Apple iPhone 17", │ │
│ │ "default_sku": { // 主推SKU(默认规格) │ │
│ │ "sku_id": "SKU001", │ │
│ │ "spec": "黑色 128GB", // 默认规格 │ │
│ │ "base_price": 7999.00, // 基础价格 │ │
│ │ "promo_price": 7599.00, // 促销价(秒杀95折) │ │
│ │ "saved": 400.00, │ │
│ │ "promo_label": "限时95折" │ │
│ │ }, │ │
│ │ "price_range": "¥7599 - ¥10999", // 所有SKU价格区间 │ │
│ │ "stock_status": "现货" │ │
│ │ } │ │
│ │ ] │ │
│ │ │ │
│ │ ES数据结构: │ │
│ │ { │ │
│ │ "item_id": "ITEM001", │ │
│ │ "title": "Apple iPhone 17", │ │
│ │ "default_sku_id": "SKU001", // 主推SKU │ │
│ │ "default_sku_price": 7599.00, // 主推SKU促销价 │ │
│ │ "sku_price_range": { // 所有SKU价格区间 │ │
│ │ "min": 7599.00, // 128GB黑色 │ │
│ │ "max": 10999.00 // 1TB深空紫 │ │
│ │ }, │ │
│ │ "sku_count": 12, // 12个SKU规格 │ │
│ │ "category": "手机数码" │ │
│ │ } │ │
│ │ │ │
│ │ 关键差异(vs Hotel): │ │
│ │ • ES存储主推SKU的确定价格(不是"最低价起") │ │
│ │ • 价格不依赖日期,相对稳定 │ │
│ │ • 可以直接展示促销价(7599元,而不是"7599起") │ │
│ │ │ │
│ │ 数据来源:ES缓存(异步更新) │ │
│ │ 性能:P95 < 30ms │ │
│ │ 价格维度:主推SKU价格(固定规格) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ↓ 用户点击"iPhone 17" │
│ │
│ 阶段2: Detail详情页 (展示所有SKU规格价格) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 用户进入商品详情页,查看不同规格 │ │
│ │ ┌────────────┐ │ │
│ │ │ [APP/Web] │ │ │
│ │ └─────┬──────┘ │ │
│ │ ↓ GET /product/detail?item_id=ITEM001 │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Aggregation Service │ │ │
│ │ └──────────┬──────────┘ │ │
│ │ ↓ 并发查询(3个服务) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │Product Center│ │ Marketing │ │ Inventory │ │ │
│ │ │ (SPU+SKUs) │ │ Service │ │ Service │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
│ │ ↓ ↓ ↓ │ │
│ │ 所有SKU信息 营销活动信息 各SKU库存 │ │
│ │ ↓ ↓ ↓ │ │
│ │ └──────────────────┴──────────────────┘ │ │
│ │ ↓ │ │
│ │ [Pricing Service] │ │
│ │ ↓ │ │
│ │ 返回详情(所有SKU + 价格 + 营销): │ │
│ │ { │ │
│ │ "item_id": "ITEM001", │ │
│ │ "title": "Apple iPhone 17", │ │
│ │ "skus": [ // 所有SKU规格 │ │
│ │ { │ │
│ │ "sku_id": "SKU001", │ │
│ │ "spec": "黑色 128GB", // 规格固定 │ │
│ │ "base_price": 7999.00, // 基础价格 │ │
│ │ "promo_price": 7599.00, // 秒杀价95折 │ │
│ │ "saved": 400.00, │ │
│ │ "promotions": [ │ │
│ │ { │ │
│ │ "id": "P001", │ │
│ │ "type": "限时秒杀", │ │
│ │ "desc": "限时95折", │ │
│ │ "discount_rate": 0.95 │ │
│ │ } │ │
│ │ ], │ │
│ │ "stock": 450, // 库存数量 │ │
│ │ "stock_status": "现货" │ │
│ │ }, │ │
│ │ { │ │
│ │ "sku_id": "SKU002", │ │
│ │ "spec": "白色 256GB", │ │
│ │ "base_price": 8999.00, │ │
│ │ "promo_price": 8549.00, // 会员95折 │ │
│ │ "saved": 450.00, │ │
│ │ "promotions": [ │ │
│ │ { │ │
│ │ "id": "P002", │ │
│ │ "type": "会员价", │ │
│ │ "desc": "VIP会员95折", │ │
│ │ "discount_rate": 0.95 │ │
│ │ } │ │
│ │ ], │ │
│ │ "stock": 280 │ │
│ │ }, │ │
│ │ { │ │
│ │ "sku_id": "SKU003", │ │
│ │ "spec": "深空紫 1TB", │ │
│ │ "base_price": 10999.00, │ │
│ │ "promo_price": 10999.00, // 无促销 │ │
│ │ "saved": 0, │ │
│ │ "stock": 50, │ │
│ │ "stock_status": "库存紧张" │ │
│ │ } │ │
│ │ ], │ │
│ │ "snapshot": { │ │
│ │ "snapshot_id": "snap:ITEM001:1744633200", │ │
│ │ "expires_at": 1744633500, // 5分钟后过期 │ │
│ │ "ttl": 300 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ 关键差异(vs Hotel): │ │
│ │ • 所有SKU价格是固定的(不随日期变化) │ │
│ │ • 一次返回所有规格的价格(12个SKU一次性展示) │ │
│ │ • 每个SKU独立库存、独立价格、独立营销 │ │
│ │ • 无需考虑"连住几晚"这样的时间维度 │ │
│ │ │ │
│ │ 数据来源:实时查询 + 生成快照(5分钟) │ │
│ │ 性能:P95 < 150ms │ │
│ │ 价格维度:SKU维度(固定规格 + 营销折扣) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ↓ 用户选择"白色 256GB x 1台",点击"立即购买" │
│ │
│ 阶段3: 试算 (单个SKU + 营销活动) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 用户选择:白色 256GB x 1台 │ │
│ │ ┌────────────┐ │ │
│ │ │ [APP/Web] │ │ │
│ │ └─────┬──────┘ │ │
│ │ ↓ POST /checkout/calculate │ │
│ │ { │ │
│ │ "items": [ │ │
│ │ { │ │
│ │ "sku_id": "SKU002", // 白色 256GB │ │
│ │ "quantity": 1 │ │
│ │ } │ │
│ │ ], │ │
│ │ "snapshot": { │ │
│ │ "snapshot_id": "snap:ITEM001:1744633200" // 携带快照 │ │
│ │ } │ │
│ │ } │ │
│ │ ↓ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Checkout Service│ │ │
│ │ └────────┬────────┘ │ │
│ │ ↓ 判断快照是否过期 │ │
│ │ ┌────────┴────────────────────────────┐ │ │
│ │ │ 未过期:使用快照数据(80ms)✨ │ │ │
│ │ │ 已过期:实时查询(200ms) │ │ │
│ │ └────────┬────────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌────────────────┐ │ │
│ │ │ Pricing Service│ │ │
│ │ └────────┬───────┘ │ │
│ │ ↓ │ │
│ │ 返回试算结果: │ │
│ │ { │ │
│ │ "can_checkout": true, │ │
│ │ "items": [ │ │
│ │ { │ │
│ │ "sku_id": "SKU002", │ │
│ │ "spec": "白色 256GB", │ │
│ │ "quantity": 1, │ │
│ │ "unit_price": 8999.00, // 单价 │ │
│ │ "subtotal": 8999.00, // 小计 │ │
│ │ "discount": 450.00, // 会员95折优惠 │ │
│ │ "final_price": 8549.00 │ │
│ │ } │ │
│ │ ], │ │
│ │ "price_breakdown": { │ │
│ │ "subtotal": 8999.00, // 商品原价 │ │
│ │ "sku_discount": 450.00, // SKU级别优惠 │ │
│ │ "total": 8549.00, // 应付总额 │ │
│ │ "saved": 450.00 │ │
│ │ }, │ │
│ │ "available_coupons": [ // 可用优惠券 │ │
│ │ { │ │
│ │ "code": "TECH500", │ │
│ │ "desc": "数码类满8000减500", │ │
│ │ "discount": 500.00 │ │
│ │ } │ │
│ │ ] │ │
│ │ } │ │
│ │ │ │
│ │ 关键差异(vs Hotel): │ │
│ │ • 价格计算简单:单价 × 数量,无需考虑日期范围 │ │
│ │ • 规格固定:颜色、内存确定后,SKU确定,价格确定 │ │
│ │ • 无连住优惠:单件商品,无"买N件"的复杂计算 │ │
│ │ │ │
│ │ 数据来源:快照(80ms)or 实时(200ms) │ │
│ │ 性能:P95 < 200ms │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ↓ 用户点击"提交订单" │
│ │
│ 阶段4、5: 创单 + 支付(与Hotel场景一致) │
│ • 创单:强制实时查询 + 预占库存 + 预扣券 │ │
│ • 支付:Coin + Voucher + 渠道费 │ │
│ • 详细流程见上文Hotel示例 │ │
└────────────────────────────────────────────────────────────────────────┘

ES存储策略对比:Hotel vs 标准电商

维度 Hotel(酒店) iPhone 17(标准电商)
ES存储粒度 酒店维度(Hotel维度) SPU+主推SKU维度
价格字段 lowest_price(最低价起) default_sku_price(主推SKU确定价格)
价格依赖 ✅ 依赖日期(价格日历) ❌ 不依赖日期(固定价格)
价格变化频率 高(每天可能不同) 低(月度调价)
ES存储大小 小(只存最低价) 中(存主推SKU+价格区间)
精确计算时机 详情页(查Redis) 详情页(查Product Center)
计算复杂度 高(多日期求和) 低(单价 × 数量)

ES数据结构详细对比

Hotel在ES中的存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"hotel_id": "H001",
"hotel_name": "上海和平饭店",
"city": "上海",
"lowest_price": 1299.00, // ← 参考价(所有房型所有日期最低)
"lowest_room_type": "标准大床房",
"available_date_range": {
"start": "2026-05-01",
"end": "2026-12-31"
},

// 注意:不存储具体日期的价格!
// 具体日期价格在Redis中查询
}

iPhone 17在ES中的存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"item_id": "ITEM001",
"title": "Apple iPhone 17",
"category": "手机数码",
"brand": "Apple",

// 主推SKU信息(默认规格)
"default_sku": {
"sku_id": "SKU001",
"spec": "黑色 128GB",
"base_price": 7999.00, // ← 确定价格(不是"起"价)
"promo_price": 7599.00, // 促销价
"stock_status": "现货"
},

// 所有SKU的价格区间
"sku_price_range": {
"min": 7599.00, // 128GB黑色(最便宜)
"max": 10999.00 // 1TB深空紫(最贵)
},

"sku_count": 12, // 12个SKU规格

// 所有SKU列表(可选,可以不存ES)
"skus": [
{
"sku_id": "SKU001",
"spec": "黑色 128GB",
"price": 7599.00, // ← 确定价格
"stock": 450
},
{
"sku_id": "SKU002",
"spec": "白色 256GB",
"price": 8549.00, // ← 确定价格
"stock": 280
}
// ... 其他10个SKU
]
}

关键差异总结

Hotel搜索页 → 详情页的价格流转

1
2
3
4
5
6
7
8
9
Search列表页(ES):
显示:"上海和平饭店 ¥1299起"
↓ (这是一个参考价,真实价格需要查询)
Detail详情页(Redis):
查询:2026-05-01 ~ 2026-05-03的价格日历
计算:¥1599 + ¥1799 = ¥3398(2晚)
显示:"标准大床房 ¥3398"

价格可能不一致!因为"1299起"只是最低价参考

iPhone 17搜索页 → 详情页的价格流转

1
2
3
4
5
6
7
8
9
10
11
Search列表页(ES):
显示:"iPhone 17 黑色128GB ¥7599"
↓ (这是主推SKU的确定价格)
Detail详情页(Product Center):
查询:所有SKU的价格
显示:
• 黑色 128GB ¥7599
• 白色 256GB ¥8549
• 深空紫 1TB ¥10999

价格一致!主推SKU在搜索页和详情页价格相同

为什么有这样的差异?

Hotel价格的特殊性

1
2
3
4
5
6
7
8
9
10
11
12
房价 = f(日期, 房型, 间夜数)
= 动态计算

问题:
• 日期组合太多:365天 × 364种可能的间夜数组合
• ES无法存储所有日期组合的价格
• 只能存储"最低价起"作为参考

解决方案:
• ES:存最低价(用于排序)
• Redis:存价格日历(每天的价格)
• 详情页:动态计算(sum 多天价格)

iPhone 17价格的简单性

1
2
3
4
5
6
7
8
9
10
11
12
SKU价格 = 固定值
= 不随时间变化(除非运营调价)

优势:
• 每个SKU价格确定:黑色128GB = ¥7599
• 可以直接存储在ES中
• 搜索页和详情页价格一致

ES存储方案:
• 方案A:存所有SKU价格(12个SKU全部存ES)
• 方案B:只存主推SKU价格(1个SKU)✅ 推荐
• 方案C:只存价格区间(¥7599-¥10999)

关键决策:ES是否需要存储SKU价格?

用户质疑:iPhone手机这种标准电商商品,可以查缓存(Redis)或DB(MySQL)的价格,没必要在ES中存储SKU价格吧?

这是一个非常好的架构设计问题! 让我们详细分析:


方案对比:ES存价格 vs 不存价格
方案 实现方式 优点 缺点 适用场景
方案A:ES存价格 ES中存储主推SKU价格 ✅ 搜索快(一次查询返回)
✅ 可按价格排序
✅ 可按价格区间筛选
❌ 价格变化需更新ES
❌ 数据冗余
❌ 可能不一致(更新延迟)
大型电商平台(QPS高)
方案B:ES不存价格 ES只存item_id,价格查Redis/MySQL ✅ 数据一致性好
✅ 无需更新ES
✅ 存储成本低
❌ 需要二次查询(N+1问题)
❌ 响应时间增加
中小型电商(QPS低)

性能对比分析

方案A:ES存价格

1
2
3
4
5
6
7
8
9
10
搜索流程:
[APP] → [Search Service] → [ES]
↓ 一次查询返回完整数据
返回:20个商品 + 价格

响应时间:
ES查询:30ms
总耗时:30ms ✨

优点:极致性能

方案B:ES不存价格

1
2
3
4
5
6
7
8
9
10
11
12
13
搜索流程:
[APP] → [Search Service] → [ES]
↓ 返回20个item_id
[Search Service] → [Product Center] / [Redis]
↓ 批量查询20个商品的价格
返回:20个商品 + 价格

响应时间:
ES查询:30ms
批量查价格:50ms(Redis)or 80ms(MySQL)
总耗时:80-110ms ⚠️

问题:性能下降,但数据更准确

实际案例对比

淘宝/京东(大型平台)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
方案:ES存价格 ✅
理由:
• QPS极高(搜索QPS > 10万)
• 50ms的性能差异 × 10万QPS = 巨大成本
• 愿意接受价格延迟(1-5分钟)

ES数据:
{
"item_id": "ITEM001",
"title": "iPhone 17",
"price": 7599.00, // ← 存在ES
"promo_price": 7599.00,
"updated_at": "2026-04-15 10:00:00"
}

更新机制:
• 价格变化 → Kafka → ES Updater → 异步更新ES
• 延迟1-5分钟可接受
• 用户在详情页看到的是最新价格(实时查询)

小型电商平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
方案:ES不存价格 ✅
理由:
• QPS较低(搜索QPS < 1000)
• 80ms vs 30ms的差异可接受
• 数据一致性更重要

ES数据:
{
"item_id": "ITEM001",
"title": "iPhone 17",
"category": "手机数码"
// ❌ 不存价格
}

查询流程:
• ES返回item_id列表
• 批量查询Redis/MySQL获取价格
• 总耗时:80ms

推荐方案:混合策略(最佳实践)✅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ====== 搜索服务实现 ======
func (s *SearchService) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
// Step 1: 查询ES获取商品列表
esResult, _ := s.esClient.Search(ctx, req.Keyword)

// Step 2: 判断是否需要查询价格
var items []*SearchItem

if s.config.EnableESPrice {
// 方案A:直接使用ES中的价格(大型平台)
for _, hit := range esResult.Hits {
items = append(items, &SearchItem{
ItemID: hit.ItemID,
Title: hit.Title,
Price: hit.Price, // ← 直接用ES价格
Stock: "现货", // 库存状态可选查询
})
}
// 性能:30ms ✨

} else {
// 方案B:二次查询价格(中小平台)✅
itemIDs := extractItemIDs(esResult)

// 批量查询价格(Redis)
prices, _ := s.priceCache.BatchGetPrices(ctx, itemIDs)

for i, hit := range esResult.Hits {
items = append(items, &SearchItem{
ItemID: hit.ItemID,
Title: hit.Title,
Price: prices[i].Price, // ← 从Redis查询
Stock: "现货",
})
}
// 性能:30ms(ES) + 50ms(Redis) = 80ms ⚠️
}

return &SearchResponse{Items: items}, nil
}

我的建议:方案B(ES不存价格)✅

理由

1. 数据一致性更重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
场景:运营修改iPhone 17价格
10:00 价格从 ¥7999 改为 ¥7599
10:01 更新MySQL → 成功
10:01 更新Redis → 成功(1秒内)
10:03 更新ES → 成功(2分钟后)

问题(方案A):
10:01 - 10:03 这2分钟内:
• 搜索页显示:¥7999(ES旧价格)
• 详情页显示:¥7599(Redis新价格)
• 用户投诉:"为什么价格不一样?"

解决(方案B):
搜索页和详情页都查Redis
→ 价格始终一致 ✅

2. 现代搜索架构可以承受二次查询

1
2
3
4
5
优化策略:
• 批量查询(BatchGetPrices):一次RPC查20个商品
• Redis性能:单次批量查询20个key < 50ms
• 总耗时:80ms vs 30ms,差异50ms
• 对于大部分场景可接受

3. ES的核心职责是搜索,不是存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ES擅长:
✅ 全文检索(关键词搜索)
✅ 多维筛选(品类、品牌、价格区间)
✅ 排序(销量、评分、价格)

ES不擅长:
❌ 强一致性(更新延迟)
❌ 频繁更新(价格经常变)
❌ 作为数据源(应该是索引)

设计原则:
"ES是索引,不是数据源"
→ ES存item_id + 标题 + 品类(用于搜索)
→ 价格、库存从Redis/MySQL查询

4. 价格区间筛选的替代方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
用户筛选:价格 ¥5000-¥10000

方案A(ES存价格):
ES Query: {"range": {"price": {"gte": 5000, "lte": 10000}}}
→ 直接在ES中筛选
→ 快,但可能不准确

方案B(ES不存价格)✅:
Step 1: ES返回所有商品(或按价格区间存标签)
Step 2: Redis批量查价格
Step 3: 在应用层筛选价格区间
→ 稍慢,但准确

折中方案:
ES存价格区间标签:
{
"item_id": "ITEM001",
"price_tag": "5k-10k" // 粗粒度价格区间
}
→ ES按标签筛选(快速)
→ Redis查精确价格(准确)

更新后的ES存储建议

标准电商商品(iPhone 17) - 推荐方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ✅ 推荐:ES不存价格,只存搜索必要信息
{
"item_id": "ITEM001",
"title": "Apple iPhone 17 黑色 128GB",
"category": "手机数码",
"brand": "Apple",
"default_sku_id": "SKU001", // 主推SKU

// 价格区间标签(粗粒度,用于筛选)
"price_range_tag": "5k-10k", // ← 区间标签,非精确价格

"rating": 4.9,
"sales": 125800,
"tags": ["5G", "双卡"],

// ❌ 不存储价格(price字段)
// 价格从Redis/MySQL查询
}

查询流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Step 1: 查询ES
GET /product_search/_search
{
"query": {"match": {"title": "iPhone 17"}},
"filter": {"term": {"price_range_tag": "5k-10k"}},
"sort": [{"sales": "desc"}], // 按销量排序
"size": 20
}
↓ 返回:20个item_id

Step 2: 批量查询Redis价格
MGET product:price:ITEM001
product:price:ITEM002
...
product:price:ITEM020
↓ 返回:20个商品的价格

Step 3: 合并数据
[{
"item_id": "ITEM001",
"title": "iPhone 17",
"price": 7599.00, // ← 从Redis查询
"stock": "现货"
}]

总耗时:30ms(ES) + 50ms(Redis) = 80ms

性能优化建议

如果觉得80ms慢,可以优化

优化1:Redis Pipeline批量查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (s *SearchService) BatchGetPrices(itemIDs []string) (map[string]float64, error) {
pipe := s.redis.Pipeline()

// 批量查询(一次网络往返)
cmds := make([]*redis.StringCmd, len(itemIDs))
for i, id := range itemIDs {
key := fmt.Sprintf("product:price:%s", id)
cmds[i] = pipe.Get(ctx, key)
}

pipe.Exec(ctx)

// 解析结果
prices := make(map[string]float64)
for i, cmd := range cmds {
price, _ := cmd.Float64()
prices[itemIDs[i]] = price
}

return prices, nil
}
// 性能:20个key < 10ms ✨

优化2:本地缓存(应用层)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在Search Service本地缓存热门商品价格
type LocalCache struct {
cache *freecache.Cache // 100MB本地缓存
}

func (s *SearchService) GetPriceWithCache(itemID string) float64 {
// Step 1: 查本地缓存(<1ms)
if price, ok := s.localCache.Get(itemID); ok {
return price
}

// Step 2: 查Redis(10ms)
price := s.redis.Get(ctx, "product:price:" + itemID)

// Step 3: 写入本地缓存(TTL 30秒)
s.localCache.Set(itemID, price, 30)

return price
}

// 热门商品命中率:90%
// 平均响应时间:30ms(ES) + 1ms(本地缓存) = 31ms ✨

优化3:按价格排序时,才需要价格数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (s *SearchService) Search(req *SearchRequest) (*SearchResponse, error) {
if req.SortBy == "price" {
// 场景1:按价格排序(需要精确价格)
// 方案:查询Redis/MySQL,在应用层排序
// 或者:ES存价格区间标签,粗排序
items := s.searchWithPriceSort(req)
} else {
// 场景2:按销量/评分排序(价格可以懒加载)
// ES只返回item_id,前端异步查询价格
// 或者:后端批量查询价格(可并发)
items := s.searchWithDefaultSort(req)
}

return items, nil
}

实战建议:根据业务场景选择

推荐方案B(ES不存价格),当你的系统满足以下条件:

  1. QPS可控(搜索QPS < 5000)

    • 二次查询增加的50ms延迟可接受
    • Redis批量查询性能足够
  2. 价格变化频繁(每天多次调价)

    • 促销活动频繁变化
    • 秒杀价实时变化
    • ES更新延迟导致价格不一致
  3. 数据一致性要求高

    • 用户对价格敏感
    • 搜索页和详情页价格必须一致
    • 避免投诉

保留方案A(ES存价格),当你的系统满足以下条件:

  1. QPS极高(搜索QPS > 10万)

    • 50ms × 10万 = 5000秒CPU时间
    • 性能是第一优先级
  2. 价格相对稳定(每天调价<10次)

    • 更新ES的成本可控
    • 异步更新延迟可接受
  3. 可以接受搜索页价格不精确

    • 搜索页价格可以标注”¥7599起”
    • 详情页价格以实时查询为准

推荐架构:ES不存价格 + Redis/MySQL查询

ES数据结构(精简版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"item_id": "ITEM001",
"title": "Apple iPhone 17",
"category": "手机数码",
"brand": "Apple",
"default_sku_id": "SKU001",

// 价格区间标签(用于粗筛选)
"price_tag": "5k-10k", // ← 粗粒度标签

"rating": 4.9,
"sales": 125800,

// ❌ 不存储price字段
}

Redis价格缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Key: product:price:{item_id}:{sku_id}
Value: {
"base_price": 7999.00,
"promo_price": 7599.00,
"promo_id": "P001",
"updated_at": 1744633200
}
TTL: 5分钟

批量查询:
MGET product:price:ITEM001:SKU001
product:price:ITEM001:SKU002
...

响应时间:20个key < 10ms

MySQL数据源(兜底):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 价格表
CREATE TABLE product_prices (
item_id VARCHAR(32),
sku_id VARCHAR(32),
base_price DECIMAL(10, 2),
promo_price DECIMAL(10, 2),
promo_id VARCHAR(32),
updated_at TIMESTAMP,
PRIMARY KEY (item_id, sku_id),
INDEX idx_item (item_id)
);

-- Redis失效时降级查询
SELECT * FROM product_prices
WHERE item_id IN ('ITEM001', 'ITEM002', ..., 'ITEM020');

完整的查询流程(推荐实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func (s *SearchService) SearchWithPrice(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
// Step 1: 查询ES(只查商品信息,不查价格)
esResult, err := s.esClient.Search(ctx, &ESSearchRequest{
Keyword: req.Keyword,
Category: req.Category,
PriceTag: req.PriceRangeTag, // 粗筛选:5k-10k
Sort: "sales_desc", // 按销量排序(不按价格)
Size: 20,
})
if err != nil {
return nil, err
}

// Step 2: 提取item_id和default_sku_id
itemIDs := make([]string, len(esResult.Hits))
skuIDs := make([]string, len(esResult.Hits))
for i, hit := range esResult.Hits {
itemIDs[i] = hit.ItemID
skuIDs[i] = hit.DefaultSkuID
}

// Step 3: 批量查询价格(Redis,带降级)
prices, err := s.batchGetPrices(ctx, itemIDs, skuIDs)
if err != nil {
// Redis失效,降级查询MySQL
prices, _ = s.batchGetPricesFromDB(ctx, itemIDs, skuIDs)
}

// Step 4: 合并数据
items := make([]*SearchItem, len(esResult.Hits))
for i, hit := range esResult.Hits {
items[i] = &SearchItem{
ItemID: hit.ItemID,
Title: hit.Title,
SkuID: hit.DefaultSkuID,
BasePrice: prices[hit.ItemID].BasePrice, // ← 从Redis查询
PromoPrice: prices[hit.ItemID].PromoPrice, // ← 从Redis查询
Saved: prices[hit.ItemID].BasePrice - prices[hit.ItemID].PromoPrice,
Stock: "现货",
}
}

// Step 5: 按价格精确排序(如果需要)
if req.SortBy == "price" {
sort.Slice(items, func(i, j int) bool {
return items[i].PromoPrice < items[j].PromoPrice
})
}

return &SearchResponse{Items: items}, nil
}

核心结论

“ES不存价格,价格从Redis/MySQL查询”

理由

  1. ✅ 数据一致性更重要(避免搜索页vs详情页价格不一致)
  2. ✅ 价格变化频繁(促销、秒杀),ES更新延迟导致问题
  3. ✅ Redis批量查询性能足够(50ms)
  4. ✅ 可以用本地缓存进一步优化(热门商品命中率90%)
  5. ✅ ES专注于搜索职责,不承担存储职责

例外情况(可以考虑ES存价格):

  • QPS极高(>10万)且对50ms延迟敏感
  • 价格相对稳定(每天调价<10次)
  • 可以接受1-5分钟的价格延迟

Hotel(酒店) - ES不存精确价格,只存参考价:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Hotel在ES中(只存参考价,用于搜索结果排序展示)
{
"hotel_id": "H001",
"hotel_name": "上海和平饭店",
"city": "上海",

// ✅ 存最低参考价(用于排序和展示"¥1299起")
"lowest_price": 1299.00, // ← 参考价(用于排序)
"price_range": {
"min": 1299.00,
"max": 3999.00
},

"available_date_range": {
"start": "2026-05-01",
"end": "2026-12-31"
},

"rating": 4.8,
"location": "外滩",

// ❌ 不存储具体日期的精确价格
// ❌ 不存储价格日历
// ❌ 不存储不同房型的价格

// 精确价格从Redis价格日历查询:
// Redis Key: hotel:price_calendar:H001
// {
// "2026-05-01": {"single": 1299, "double": 1899},
// "2026-05-02": {"single": 1499, "double": 2199},
// ...
// }
}

查询流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Step 1: 查询ES
GET /hotel_search/_search
{
"query": {"match": {"city": "上海"}},
"filter": {"range": {"lowest_price": {"lte": 3000}}},
"sort": [{"lowest_price": "asc"}], // 按参考价排序
"size": 20
}
↓ 返回:20个hotel_id,每个带参考价(¥1299起)

Step 2: 批量查询Redis价格日历(可选)
// 如果用户已选择日期,批量查询精确价格
MGET hotel:price_calendar:H001
hotel:price_calendar:H002
...
↓ 返回:20个酒店的价格日历

Step 3: 计算用户选择日期的精确价格
// 根据用户选择的入住日期、间夜计算总价
// 例如:2026-05-01入住,2晚
// = ¥1299(第1晚) + ¥1499(第2晚) = ¥2798

总耗时:30ms(ES) + 50ms(Redis) + 10ms(计算) = 90ms

Hotel的特殊性

  • 不能只存一个价格:酒店价格随日期变化
  • ES存参考价:用于搜索结果排序(¥1299起)
  • Redis存价格日历:用于用户选择日期后的精确计算
  • MySQL存价格规则:早鸟价、周末价、假日价等

阶段1:搜索列表(Search List)

场景:用户搜索”无线耳机”,展示商品列表(每页20个商品)

价格计算范围

1
2
3
4
5
✅ 基础价格(base_price)
✅ 营销折扣价(promo_price,如果有)
❌ 不计算优惠券(用户还未选择)
❌ 不计算Coin(用户还未选择)
❌ 不计算支付渠道费(未到支付阶段)

数据来源

  • Elasticsearch缓存(搜索索引)
  • 价格数据已预先写入ES,不实时计算
  • 异步更新:价格变化 → Kafka → Search Service → 更新ES

系统交互

1
2
3
4
5
6
7
8
9
10
11
12
13
[APP/Web]
↓ GET /search?keyword=无线耳机
[Aggregation Service]
↓ RPC: SearchES(keyword)
[Search Service]
↓ Query Elasticsearch
[Elasticsearch]
↓ 返回:sku_id, title, base_price, promo_price, image
[Search Service]

[Aggregation Service]
↓ 聚合库存、销量(可选)
[APP/Web] ← 返回搜索结果

响应示例

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"items": [
{
"sku_id": 1001,
"title": "AirPods Pro",
"base_price": 1999.00, // 基础价格
"promo_price": 1799.00, // 营销折扣价(秒杀/限时购)
"discount_label": "限时9折", // 营销标签
"stock_status": "现货",
"sales": 12580
}
]
}

关键特点

  • 极致性能:P95 < 30ms(ES查询)
  • 📦 批量展示:20-50个商品
  • 🔄 异步更新:价格变化不实时同步(可能延迟1-5分钟)
  • 🎯 简单价格:只展示基础价和促销价,不涉及用户个性化

阶段2:商品详情页(Product Detail Page)

场景:用户点击商品进入详情页,选择SKU规格

价格计算范围

1
2
3
4
5
6
✅ 基础价格(base_price)
✅ 营销折扣(限时购、秒杀、满减预告)
✅ 用户专享价(会员价、新人价)
❌ 不计算优惠券(需要用户主动选择)
❌ 不计算Coin(需要用户主动选择)
❌ 不计算支付渠道费(未到支付阶段)

数据来源

  • 实时查询:Product Center + Marketing Service
  • 生成快照:将查询结果缓存5分钟(snapshot_id)
  • 用户ID参与计算(个性化价格)

系统交互

1
2
3
4
5
6
7
8
9
10
11
[APP/Web]
↓ GET /product/detail?sku_id=1001&user_id=67890
[Aggregation Service]
↓ 并发查询(3个服务)
├─→ [Product Center]: 获取商品基础信息
├─→ [Marketing Service]: 获取该用户可享受的促销
└─→ [Inventory Service]: 获取库存状态
↓ 聚合数据
↓ 调用 [Pricing Service]: CalculatePrice(base_price, promos)
↓ 生成 snapshot_id(快照ID,5分钟有效)
[APP/Web] ← 返回详情 + 价格 + 快照

响应示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"sku_id": 1001,
"title": "AirPods Pro",
"base_price": 1999.00,
"final_price": 1699.00, // 最终价格(含营销折扣)
"saved": 300.00, // 节省金额
"promotions": [
{
"id": "P001",
"type": "限时购",
"desc": "限时9折",
"discount": 200.00
},
{
"id": "P002",
"type": "会员价",
"desc": "会员专享95折",
"discount": 100.00
}
],
"snapshot": {
"snapshot_id": "snap:1001:1744633200",
"created_at": 1744633200,
"expires_at": 1744633500, // 5分钟后过期
"ttl": 300
},
"stock": {
"available": 450,
"status": "现货充足"
}
}

关键特点

  • 🎯 个性化价格:基于user_id计算(会员价、新人价)
  • 💾 生成快照:缓存5分钟,供后续试算使用(ADR-008)
  • 性能可控:P95 < 150ms(3个RPC并发)
  • 📊 完整信息:展示价格明细和促销原因

阶段3:加购试算(Checkout Calculate)

场景:用户选择多个商品,点击”去结算”,查看总价

价格计算范围

1
2
3
4
5
6
7
8
9
✅ 商品基础价格(多SKU合计)
✅ 商品级别营销(单品折扣、限时购)
✅ 品类级别营销(品类满减、买N件M折)
✅ 订单级别营销(满减、满折)
⚠️ 优惠券预览(可选,用户主动选择)
❌ 不扣减优惠券(仅预览)
❌ 不计算Coin(用户还未选择)
❌ 不计算运费(需要地址信息)
❌ 不计算支付渠道费(未选择支付方式)

数据来源

  • 可使用快照(ADR-008):如果快照未过期(5分钟内)
  • 快照过期则实时查询:Product + Marketing Service
  • 库存必须实时:不能使用快照

系统交互

1
2
3
4
5
6
7
8
9
10
11
12
[APP/Web]
↓ POST /checkout/calculate
↓ 携带:items[], snapshot(可选)
[Checkout Service]
↓ 判断快照是否过期
├─→ 未过期:使用快照数据(80ms)✨
└─→ 已过期:实时查询(230ms)
├─→ [Product Center]: BatchGetProducts
├─→ [Marketing Service]: GetPromotions
└─→ [Inventory Service]: BatchCheckStock(必须实时)
↓ 调用 [Pricing Service]: CalculateFinalPrice(items, promos)
[APP/Web] ← 返回试算结果

响应示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"can_checkout": true,
"items": [
{
"sku_id": 1001,
"quantity": 2,
"unit_price": 1999.00,
"subtotal": 3998.00,
"discount": 399.80, // 9折优惠
"final_price": 3598.20
},
{
"sku_id": 1005,
"quantity": 1,
"unit_price": 89.00,
"subtotal": 89.00,
"discount": 0,
"final_price": 89.00
}
],
"price_breakdown": {
"subtotal": 4087.00, // 商品原价合计
"item_discount": 399.80, // 商品级优惠
"order_discount": 50.00, // 订单级优惠(满300减50)
"coupon_preview": 50.00, // 优惠券预览(可用)
"total": 3637.20, // 应付总额(不含优惠券)
"total_with_coupon": 3587.20, // 使用优惠券后的价格
"saved": 449.80
},
"available_coupons": [ // 可用优惠券列表
{
"code": "SAVE50",
"desc": "满500减50",
"discount": 50.00
}
]
}

关键特点

  • 性能优化:快照命中率80%,响应时间80ms(vs 230ms)
  • 🔄 允许降级:营销服务失败 → 移除失效促销,继续计算
  • 📊 价格明细:展示每一层优惠的具体金额
  • 🎫 优惠券预览:告知用户可用的优惠券(不扣减)

阶段4:创建订单(Create Order)

场景:用户点击”提交订单”,锁定库存和价格

价格计算范围

1
2
3
4
5
6
7
8
9
✅ 商品基础价格
✅ 商品级别营销
✅ 品类级别营销
✅ 订单级别营销
✅ 优惠券折扣(用户选择的券)
⚠️ 运费(如果有地址信息)
⚠️ 服务费(如果需要)
❌ 不计算Coin(在支付阶段计算)
❌ 不计算支付渠道费(在支付阶段计算)

数据来源

  • 强制实时查询(ADR-009):绝不使用快照
  • 价格校验(ADR-011):对比前端期望价格
  • 库存预占(ADR-002):CAS原子操作

系统交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[APP/Web]
↓ POST /order/create
↓ 携带:items[], expected_price, coupon_codes[]
[Order Service]
↓ Step 1: 强制实时查询(不使用快照)✅
├─→ [Product Center]: BatchGetProducts
├─→ [Marketing Service]: GetPromotions
└─→ 校验活动有效性(完整校验)
↓ Step 2: 预占库存(CAS操作)✅
└─→ [Inventory Service]: ReserveStock
↓ Step 3: 预扣优惠券✅
└─→ [Marketing Service]: ReserveCoupon
↓ Step 4: 实时计算价格✅
└─→ [Pricing Service]: CalculateFinalPrice
↓ Step 5: 价格校验✅
└─→ 对比 actual_price vs expected_price
├─→ 差异 > 1元 → 返回错误,要求用户确认
└─→ 差异 ≤ 1元 → 继续创单
↓ Step 6: 创建订单(状态:PENDING_PAYMENT)
[APP/Web] ← 返回订单ID + 实际价格

响应示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"order_id": "ORD202604151234567890",
"status": "PENDING_PAYMENT",
"items": [...],
"price_breakdown": {
"subtotal": 4087.00,
"item_discount": 399.80,
"order_discount": 50.00,
"coupon_discount": 50.00, // 优惠券已预扣
"shipping_fee": 10.00, // 运费
"service_fee": 5.00, // 服务费
"total": 3602.20, // 应付总额(不含Coin和渠道费)
"saved": 499.80
},
"reserved_resources": {
"stock_ids": ["RSV123", "RSV456"],
"coupon_ids": ["CPN789"]
},
"expires_at": 1744634100 // 15分钟后过期(超时自动取消)
}

关键特点

  • 🔒 资源锁定:库存预占、优惠券预扣(15分钟超时释放)
  • 强制实时:绝不使用快照,保证价格准确(ADR-009)
  • 🛡️ 价格校验:对比期望价格,差异>1元需用户确认(ADR-011)
  • ⚠️ 严格失败:营销失效 → 拒绝创单(不降级)

阶段5:支付计算(Payment Calculate & Create)

场景:用户在支付页选择Coin、Voucher、支付方式,查看最终金额

价格计算范围

1
2
3
4
5
✅ 订单金额(from Order)
✅ Coin抵扣(用户选择使用的Coin)
✅ Voucher抵扣(平台代金券)
✅ 支付渠道费(信用卡手续费、分期费)
✅ 最终应付金额 = 订单金额 - Coin - Voucher + 渠道费

数据来源

  • 订单金额:从Order Service读取(已锁定)
  • Coin余额:实时查询 User Service
  • Voucher:实时查询 Marketing Service
  • 支付渠道费率:Payment Gateway配置

系统交互

5.1 支付前试算(用户选择Coin/Voucher时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[APP/Web]
↓ POST /payment/calculate
↓ 携带:order_id, coin_amount, voucher_codes[], payment_method
[Payment Service]
↓ Step 1: 查询订单金额
└─→ [Order Service]: GetOrder(order_id)
└─→ 返回:total = 3602.20元
↓ Step 2: 校验Coin余额
└─→ [User Service]: GetCoinBalance(user_id)
└─→ 可用Coin:500个(1 Coin = ¥1)
↓ Step 3: 校验Voucher有效性
└─→ [Marketing Service]: ValidateVoucher(voucher_codes)
└─→ 可用:满3000减100
↓ Step 4: 计算支付渠道费
└─→ 查询Payment Gateway配置
└─→ 信用卡分期:0.6%手续费
↓ Step 5: 计算最终金额
订单金额: 3602.20元
- Coin抵扣: -100.00元(使用100个Coin)
- Voucher: -100.00元(满3000减100)
+ 渠道费: +21.01元(3402.20 × 0.6%)
= 最终应付: 3423.21元
[APP/Web] ← 返回试算结果(实时响应100-200ms)

5.2 创建支付(用户点击”确认支付”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[APP/Web]
↓ POST /payment/create
↓ 携带:order_id, coin_amount, voucher_codes[], payment_method
[Payment Service]
↓ Step 1: 后端重新计算金额(防篡改)✅
└─→ 重复上面的计算逻辑
└─→ actual_amount = 3423.21元
↓ Step 2: 对比前端期望金额
└─→ expected_amount = 3423.21元
└─→ 差异 < 0.01元 → 继续 ✅
↓ Step 3: 预扣Coin和Voucher✅
├─→ [User Service]: DeductCoin(100)
└─→ [Marketing Service]: ConsumeVoucher(voucher_codes)
↓ Step 4: 创建支付记录
└─→ INSERT INTO payments (order_id, amount, status='PENDING')
↓ Step 5: 调用支付网关
└─→ [Payment Gateway]: CreatePayment(3423.21元, method)
└─→ 返回支付URL(支付宝/微信)
[APP/Web] ← 返回支付URL,跳转到支付宝/微信

响应示例

试算响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"order_amount": 3602.20,
"coin_discount": 100.00,
"voucher_discount": 100.00,
"payment_fee": 21.01,
"final_amount": 3423.21,
"breakdown": {
"subtotal": 4087.00,
"item_discount": 399.80,
"order_discount": 50.00,
"coupon_discount": 50.00,
"shipping_fee": 10.00,
"service_fee": 5.00,
"coin_discount": 100.00,
"voucher_discount": 100.00,
"payment_fee": 21.01,
"final": 3423.21
}
}

关键特点

  • 💰 最终金额:包含所有维度(Coin + Voucher + 渠道费)
  • 🔄 实时试算:用户每次选择都重新计算(防抖100ms)
  • 🛡️ 防篡改:后端必须重新计算,不信任前端
  • 性能要求:试算P95 < 200ms,创建P95 < 300ms

全局对比表:各阶段的相同点与不同点

维度 搜索列表 商品详情页 加购试算 创建订单 支付计算
API GET /search GET /product/detail POST /checkout/calculate POST /order/create POST /payment/calculate
基础价格 ✅(已锁定)
营销折扣 ✅(缓存) ✅(已锁定)
优惠券 ❌(仅预告) ⚠️(预览) ✅(预扣) ✅(已锁定)
Coin ✅(扣减)
Voucher ✅(扣减)
运费
支付渠道费
数据来源 ES缓存 实时查询 快照 or 实时 强制实时 强制实时
个性化 ✅(user_id)
库存查询 ❌(可选) ✅(不扣) ✅(不扣) ✅(预占) N/A
资源锁定 ✅(库存+券) ✅(Coin+Voucher)
失败处理 返回空 返回错误 降级(移除失效促销) 拒绝(返回错误) 拒绝
性能目标 P95 < 30ms P95 < 150ms P95 < 230ms P95 < 500ms P95 < 200ms
调用频率 极高
缓存策略 ES预缓存 生成快照(5分钟) 使用快照 不缓存 不缓存
价格可变性 低(异步更新) 中(实时但缓存5分钟) 中(快照可能过期) 低(已锁定) 低(已锁定)

系统交互关系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
┌────────────────────────────────────────────────────────────────┐
│ 价格计算在各阶段的系统交互 │
├────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: 搜索列表 │
│ ┌──────────┐ │
│ │Aggregation│──→ Search Service ──→ Elasticsearch │
│ │ Service │ └→ base_price, promo │
│ └──────────┘ │
│ ↓ 30ms │
│ │
│ Phase 2: 商品详情页 │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │Aggregation│──→ │ Product │ │Marketing│ │
│ │ Service │──→ │ Center │ │ Service │ │
│ └──────────┘ └──────────┘ └─────────┘ │
│ ↓ ↓ ↓ │
│ └────────────────┴────────────────┘ │
│ ↓ │
│ [Pricing Service] │
│ ↓ │
│ 生成 snapshot_id │
│ ↓ 150ms │
│ │
│ Phase 3: 加购试算 │
│ ┌──────────┐ │
│ │Checkout │──→ 判断 snapshot 是否过期 │
│ │ Service │ ├─→ 未过期:使用快照(80ms) │
│ └──────────┘ └─→ 已过期:实时查询(230ms) │
│ ↓ ├─→ Product Center │
│ └───────────────→├─→ Marketing Service │
│ └─→ Inventory Service(必须实时) │
│ ↓ │
│ [Pricing Service] │
│ ↓ 80-230ms │
│ │
│ Phase 4: 创建订单 │
│ ┌──────────┐ │
│ │ Order │──→ 强制实时查询(不用快照) │
│ │ Service │ ├─→ Product Center │
│ └──────────┘ ├─→ Marketing Service(完整校验) │
│ ↓ ├─→ Inventory Service(预占库存)CAS │
│ └──────────→└─→ Marketing Service(预扣优惠券) │
│ ↓ │
│ [Pricing Service] │
│ ↓ │
│ 价格校验(vs expected_price) │
│ ↓ 500ms │
│ │
│ Phase 5: 支付计算 │
│ ┌──────────┐ │
│ │ Payment │──→ Order Service(获取订单金额) │
│ │ Service │──→ User Service(Coin余额) │
│ └──────────┘──→ Marketing Service(Voucher校验) │
│ ↓ ──→ Payment Gateway(渠道费率) │
│ └──────────────────┴───────────────┘ │
│ ↓ │
│ 计算最终支付金额 │
│ = 订单 - Coin - Voucher + 渠道费 │
│ ↓ 200ms │
└────────────────────────────────────────────────────────────────┘

关键设计原则

原则1:分阶段计算,逐步扩展价格维度

1
2
3
4
5
搜索:       基础价格 + 营销折扣
详情: 基础价格 + 营销折扣(个性化)
试算: 基础价格 + 营销折扣 + 优惠券(预览)
创单: 基础价格 + 营销折扣 + 优惠券 + 运费
支付: 订单金额 + Coin + Voucher + 渠道费

原则2:数据来源逐步收紧,保证最终准确

1
2
3
4
5
搜索:       ES缓存(异步更新,允许延迟)
详情: 实时查询 → 生成快照
试算: 快照(性能优先) or 实时(过期降级)
创单: 强制实时(安全优先)
支付: 强制实时(最终校验)

原则3:资源锁定逐步加强,防止超卖

1
2
3
4
5
搜索:       不锁定
详情: 不锁定
试算: 不锁定(仅查询)
创单: 预占库存 + 预扣优惠券(15分钟)
支付: 扣减Coin + 消费Voucher

原则4:性能与准确性平衡,分场景优化

1
2
3
4
5
搜索:       极致性能(30ms)  → ES缓存
详情: 性能优先(150ms) → 生成快照
试算: 性能优先(80-230ms)→ 使用快照
创单: 准确性优先(500ms)→ 强制实时
支付: 准确性优先(200ms)→ 强制实时

常见问题与答案

Q1:为什么搜索列表的价格和详情页可能不一样?

  • 搜索列表:ES缓存,异步更新(延迟1-5分钟)
  • 详情页:实时查询,包含用户个性化价格(会员价)
  • 结论:正常现象,用户可以理解

Q2:详情页的快照会过期吗?试算价格会变吗?

  • 快照有效期5分钟
  • 如果用户5分钟内进入试算 → 使用快照,价格一致
  • 如果超过5分钟 → 重新查询,价格可能变化
  • 创单时会强制实时查询,最终以创单价格为准

Q3:试算价格和创单价格可能不同吗?

  • 可能不同的情况:
    1. 活动在试算和创单之间结束了
    2. 活动库存在试算和创单之间用完了
    3. 优惠券被其他订单消费了
  • 解决方案:创单时对比价格,差异>1元需用户确认(ADR-011)

Q4:Coin和Voucher为什么在支付阶段才计算?

  • Coin和Voucher是用户在支付页主动选择的
  • 创单时还不知道用户会选择哪些
  • 支付阶段才是最终确定的时机

Q5:支付渠道费为什么不在创单时计算?

  • 用户可能在支付页更换支付方式(信用卡、分期、余额)
  • 不同支付方式的手续费不同
  • 支付阶段才能确定最终的支付方式

监控指标

价格一致性监控

1
2
3
- 试算vs创单价格差异率(目标 < 5%)
- 创单vs支付价格差异率(目标 < 1%)
- 价格变化导致的订单取消率(目标 < 2%)

性能监控

1
2
3
4
5
- 搜索价格展示P95(目标 < 30ms)
- 详情页价格计算P95(目标 < 150ms)
- 试算价格计算P95(目标 < 230ms)
- 创单价格计算P95(目标 < 500ms)
- 支付试算P95(目标 < 200ms)

快照效率监控

1
2
3
- 快照命中率(目标 > 80%)
- 快照过期率(目标 < 20%)
- 快照过期导致的RT增加(目标 < 150ms)

核心要点总结

“分阶段计算,逐步扩展,最终强制校验”

  1. 搜索阶段:ES缓存,极致性能(30ms)
  2. 详情阶段:实时查询,生成快照(150ms)
  3. 试算阶段:使用快照,性能优先(80-230ms)
  4. 创单阶段:强制实时,安全优先(500ms)
  5. 支付阶段:最终校验,包含所有维度(200ms)
  6. 价格维度:逐步扩展(基础 → 营销 → 券 → Coin → 渠道费)
  7. 资源锁定:逐步加强(不锁 → 预占 → 扣减)

六、部署架构(同城双活)

6.1 整体拓扑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
                       互联网用户流量

┌─────────────────────────────┐
│ 全局DNS(GeoDNS) │
│ • 智能解析(就近接入) │
│ • 健康检查(故障切换) │
└─────────────────────────────┘
↓ ↓
┌──────────────┴──────────┴──────────────┐
↓ ↓
┌─────────────────────────┐ ┌─────────────────────────┐
│ IDC-A(主机房) │ │ IDC-B(备机房) │
│ 同城10km内 │◄─────│ 同城10km内 │
│ │ 双向 │ │
│ • K8s Cluster (3M+50W) │ 同步 │ • K8s Cluster (3M+50W) │
│ • MySQL 主库(写) │◄────►│ • MySQL 从库(读) │
│ • Redis Cluster (8主8从)│ │ • Redis Cluster (8主8从)│
│ • Kafka (6 Broker) │◄MM2─►│ • Kafka (6 Broker) │
│ • Elasticsearch (6节点) │ │ • Elasticsearch (6节点) │
│ │ │ │
│ 流量占比:60% │ │ 流量占比:40% │
└─────────────────────────┘ └─────────────────────────┘
网络延迟:< 2ms(专线连接)

6.2 MySQL双主部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IDC-A                                 IDC-B
┌─────────────────────┐ ┌─────────────────────┐
│ MySQL Master A │◄────双向────►│ MySQL Master B │
│ • server_id: 1 │ binlog同步 │ • server_id: 2 │
│ • auto_increment_ │ │ • auto_increment_ │
│ offset: 1 │ │ offset: 2 │
│ • auto_increment_ │ │ • auto_increment_ │
│ increment: 2 │ │ increment: 2 │
│ • 承载60%写流量 │ │ • 承载40%写流量 │
└─────────────────────┘ └─────────────────────┘
↓ ↓
┌─────────────────────┐ ┌─────────────────────┐
│ MySQL Slave A1/A2 │ │ MySQL Slave B1/B2 │
│ • 只读(Read) │ │ • 只读(Read) │
│ • 延迟监控<1s │ │ • 延迟监控<1s │
└─────────────────────┘ └─────────────────────┘

关键配置

  • 半同步复制:保证数据不丢失(1秒超时降级为异步)
  • GTID模式:简化主从切换
  • 自增ID错开:避免主键冲突(offset+increment)

6.3 容灾切换SOP

场景1:IDC-A计划性维护

1
2
3
4
5
6
7
T-60min: 提前通知,确认IDC-B容量充足
T-30min: 检查双机房数据同步状态
T-15min: DNS权重调整 A:60%→30%,B:40%→70%
T-10min: 继续调整 A:30%→0%,B:70%→100%
T-5min: 确认IDC-A无流量,停止服务
T0: 开始维护
T+Xmin: 维护完成,逆向切回流量

场景2:IDC-A突发故障

1
2
3
4
5
6
T0:      监控发现IDC-A全量服务不可用
T+30s: 告警触发,呼叫值班人员
T+2min: 人工确认故障范围,决策切换
T+3min: 执行DNS强制切换:A:0%,B:100%
T+5min: 确认IDC-B承载全量流量,监控关键指标
T+10min: 检查数据一致性,启动补偿任务

七、稳定性保障体系

7.1 监控体系(四层监控)

层级 监控内容 工具 告警阈值示例
L1基础设施 CPU、内存、磁盘、网络、数据库连接数、慢查询 Node Exporter + cAdvisor CPU>80%持续5分钟→P2
L2应用监控 QPS、错误率、延迟(P50/P95/P99)、资源使用率 Prometheus + Istio 错误率>1%持续2分钟→P1
L3业务监控 下单量、支付率、库存充足率、供应商可用率 自定义埋点 下单量同比下降30%→P1
L4用户体验 FCP、LCP、FID、CLS、前端错误率 Sentry / DataDog RUM LCP>4秒用户占比>10%→P2

7.2 告警分级与SLA

级别 触发条件 响应时间 恢复时间 告警方式
P0致命 核心服务不可用、超卖 <5分钟 <30分钟 电话+短信+企微@all
P1严重 核心接口错误率>5%、下单量骤降>50% <10分钟 <1小时 短信+企微+电话(3分钟未ACK)
P2一般 非核心接口错误率>10%、P99延迟>5秒 <30分钟 <4小时 企微+邮件
P3预警 磁盘使用率>80%、Redis内存>85% <1小时 当天内 企微(不@人)

核心服务SLA目标(年度)

  • Tier 1(订单/支付/结算/库存):可用性≥99.95%,P95<500ms,错误率<0.1%
  • Tier 2(商品/搜索/购物车/营销):可用性≥99.9%,P95<1s,错误率<0.5%
  • Tier 3(推荐/评价/日志):可用性≥99.5%,P95<2s,错误率<1%

7.3 限流策略(多层防护)

层级 工具 策略
L1接入层 APISIX IP限流:100req/min;API限流:5000req/s
L2用户维度 Redis + Token Bucket 下单:5次/分钟/用户;结算:10次/分钟/用户
L3服务维度 Istio order→inventory:3000req/s
L4资源维度 MySQL连接池 最大连接2000,等待超时3秒

7.4 全链路压测

压测流程(大促前3周)

  • Week 1:准备测试数据、扩容资源、配置压测标识(X-Test-Flag: pressure-test
  • Week 2:分层压测(接口单点→核心链路→全链路),目标峰值QPS 25万
  • Week 3:瓶颈分析、扩容决策、降级预案验证

压测指标

  • QPS:能否达到目标值
  • 响应时间:P95<500ms,P99<1s
  • 错误率:<0.1%
  • 资源使用:CPU<70%,内存<80%

7.5 故障演练(Chaos Engineering)

演练频率:每季度1次

演练场景

  1. 服务不可用:随机删除Pod,验证K8s自愈能力
  2. 数据库主从切换:模拟主库宕机,验证MHA自动切换
  3. 网络延迟:注入2秒延迟(Istio Fault Injection),验证超时/熔断/降级
  4. 机房故障:模拟IDC-A整体不可用,验证DNS切换和数据一致性

工具:Chaos Mesh、Litmus、Istio Fault Injection

7.6 故障复盘流程

触发条件:P0/P1故障、用户影响>10000人、故障时长>30分钟、数据丢失/错误

时间线

  • T+4h:初步复盘(电话会议),梳理时间轴、影响范围
  • T+1day:根因分析(5 Why、鱼骨图)
  • T+2day:改进计划(短期Hotfix、中期监控、长期工具)
  • T+1week:复盘文档归档、全员分享会(无责文化)

八、技术栈总结

技术领域 选型 版本 理由
编程语言 Go 1.21+ 高并发、部署简单
API网关 APISIX 3.x 性能强、插件丰富
数据库 MySQL 8.0 事务、成熟度
缓存 Redis Cluster 7.x 高性能、持久化
消息队列 Kafka 3.x 高吞吐、持久化
搜索引擎 Elasticsearch 8.x 全文搜索、聚合
RPC框架 gRPC 1.60+ 高性能、跨语言
服务网格 Istio 1.20+ 流量管理、可观测
配置中心 Nacos 2.x 动态配置、服务发现
链路追踪 Jaeger 1.50+ 分布式追踪
监控告警 Prometheus + Grafana - 指标采集、可视化
日志 ELK Stack 8.x 日志收集、分析
容器编排 Kubernetes 1.28+ 容器调度、弹性伸缩

九、成本预估

双机房总成本

资源类型 单机房月成本 双机房月成本 年成本
物理服务器(100台) ¥300,000 ¥600,000 ¥7.2M
MySQL(24实例) ¥120,000 ¥240,000 ¥2.88M
Redis(16实例) ¥32,000 ¥64,000 ¥768K
Kafka(6 Broker) ¥18,000 ¥36,000 ¥432K
Elasticsearch(6节点) ¥24,000 ¥48,000 ¥576K
网络带宽(10Gbps) ¥50,000 ¥100,000 ¥1.2M
负载均衡(F5) ¥40,000 ¥80,000 ¥960K
存储(500TB SSD) ¥250,000 ¥500,000 ¥6M
监控告警 ¥20,000 ¥40,000 ¥480K
合计 ¥854,000 ¥1.7M ¥20.5M

十、总结与展望

10.1 架构优势

  1. 高可用:同城双活+故障自动切换,核心服务SLA≥99.95%
  2. 高性能:三级缓存+分库分表+Redis原子操作,P99延迟<1秒
  3. 高扩展:微服务+K8s HPA,支持5-10倍弹性扩容
  4. 容错性强:供应商网关熔断降级+Saga补偿,单点故障不影响全局
  5. 数据一致:Saga+幂等+对账,保证订单/库存/资金强一致

10.2 技术挑战

  1. 供应商接口复杂度:50+供应商,需持续维护适配器插件
  2. 数据一致性成本:Saga补偿+对账任务,增加系统复杂度
  3. 运维复杂度:双机房部署+中间件集群,需专业SRE团队
  4. 成本控制:年成本2000万+,需持续优化资源使用率

10.3 未来演进方向

  1. 异地多活:从同城双活扩展到异地三中心(北京+上海+深圳)
  2. 智能化运维:引入AIOps,自动根因分析+自动扩缩容
  3. Serverless化:边缘服务(推荐/评价)迁移到Serverless,降本增效
  4. 全链路灰度:基于流量染色的全链路灰度发布能力

参考资料

  1. Martin Fowler - Microservices Architecture
  2. Saga Pattern - Chris Richardson
  3. Google SRE Book
  4. Alibaba技术 - 淘宝双11技术揭秘
  5. Redis官方文档
  6. Kubernetes官方文档
  7. Istio官方文档

作者:wxquare
日期:2026-04-14
版本:v1.0


十一、面试题库(资深工程师级别)

使用说明:本章节基于上述架构设计,提供70+道技术深度面试题,适合资深工程师(Staff/Principal Engineer)级别的面试准备。每个问题都包含:考察点、参考答案、追问方向、答题要点、加分项和常见误区。

题库结构

核心主题(深度准备)

  1. 价格计算引擎(18题)⭐⭐⭐ - 四层计价、营销规则、精度处理
  2. 快照机制与缓存(15题)⭐⭐⭐ - 快照设计、三级缓存、一致性
  3. 营销系统设计(12题)⭐⭐ - 营销活动、预扣机制、实时性
  4. 库存与超卖防护(15题)⭐⭐⭐ - 二维模型、预占-确认、Redis Lua

支撑主题(广度覆盖)
5. 分布式事务与一致性(8题)- Saga、幂等、补偿
6. 高并发与性能优化(8题)- 分库分表、批量优化
7. 系统容错与稳定性(6题)- 熔断降级、限流、灰度
8. 微服务架构与部署(6题)- 聚合服务、同城双活


主题一:价格计算引擎(18题)

1.1 四层计价架构设计

Q1:你们的四层计价模型是如何设计的?为什么选择这种分层方式?

考察点:架构设计能力、业务抽象能力、领域建模思维

参考答案

我们的四层计价模型按照价格形成的业务逻辑自然分层:

第一层:基础价格层(Base Price)

  • 职责:商品的基础定价,来自Product Center
  • 数据源:商品表中的base_price字段
  • 特点:变化频率低(天级),适合长时间缓存

第二层:营销促销层(Promotion)

  • 职责:应用各类营销活动(折扣、满减、限时购)
  • 数据源:Marketing Service
  • 特点:变化频率中(分钟级),需要较短TTL缓存

第三层:费用附加层(Fee)

  • 职责:平台服务费、税费、支付渠道费
  • 数据源:Pricing Service内部配置 + Payment Service
  • 特点:计算逻辑相对固定,但与支付渠道相关

第四层:优惠券/积分层(Voucher)

  • 职责:用户持有的优惠券、积分、Coin抵扣
  • 数据源:Marketing Service
  • 特点:用户相关,个性化程度最高

为什么这样分层?

  1. 单一职责原则:每层只处理一类价格因素,职责清晰
  2. 扩展性:新增变价因素只需在对应层添加,不影响其他层
  3. 性能优化:不同场景可以灵活跳层(详见下题)
  4. 缓存策略差异化:每层的缓存TTL可以独立设置

代码示例(文档4.4节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (s *PricingService) CalculateFinalPrice(items []*Item, promos []*Promotion) *PriceDetail {
// 1. 计算商品原价(基础价格层)
subtotal := calculateSubtotal(items)

// 2. 应用商品级别促销(营销层-商品维度)
itemDiscount := applyItemLevelPromotions(items, promos)

// 3. 应用品类级别促销(营销层-品类维度)
categoryDiscount := applyCategoryLevelPromotions(items, promos)

// 4. 应用订单级别促销(营销层-订单维度)
orderDiscount := applyOrderLevelPromotions(subtotal - itemDiscount - categoryDiscount, promos)

// 5. 应用优惠券(优惠券层)
couponDiscount := applyCouponPromotions(subtotal - itemDiscount - categoryDiscount - orderDiscount, promos)

// 6. 最终总价(注:费用层在支付时计算)
total := subtotal - itemDiscount - categoryDiscount - orderDiscount - couponDiscount

return &PriceDetail{...}
}

追问方向

  1. 为什么不设计成5层或3层?

    • 5层会过度设计,增加复杂度;3层无法区分营销和优惠券(业务语义不同)
    • 4层是业务分析的自然结果,符合电商价格构成的本质
  2. 如何处理层与层之间的依赖关系?

    • 采用管道模式(Pipeline Pattern),每层的输出是下一层的输入
    • 每层都是纯函数,方便单元测试
    • 使用PriceBreakdown值对象记录每层计算明细,便于追溯
  3. 如果某个促销活动同时影响多层怎么办?

    • 拆解为多个促销规则,分别在对应层生效
    • 例如”买2件8折+满300减50”拆为:商品层8折 + 订单层满减
  4. 不同场景如何灵活跳层?

    • PDP场景:只走前2层(base_price + promotion),不计算优惠券
    • Checkout场景:走3层(跳过支付渠道费)
    • Payment场景:全4层计算

答题要点

  • 业务分析驱动设计(价格因素的4种本质类型)
  • 单一职责原则(SRP)
  • 扩展性与缓存策略
  • 分场景差异化处理

加分项

  • 提及管道模式(Pipeline Pattern)
  • 提及PriceBreakdown值对象设计
  • 提及DDD领域建模思想
  • 对比其他电商的计价模型(如淘宝、京东)

常见误区

  • ❌ 回答”为了代码模块化”(过于笼统,没有业务理解)
  • ❌ 无法解释为什么是4层而不是其他数量
  • ❌ 混淆营销促销和优惠券的区别

Q2:不同场景下的计价策略有何差异?如何优化性能?

考察点:性能优化思维、场景化设计、权衡能力

参考答案

我们针对3个核心场景设计了差异化的计价策略:

场景1:商品详情页(PDP)

  • 计算范围:只计算基础价格 + 营销促销(前2层)
  • 缓存策略
    • L1本地缓存:5分钟,命中率80%+
    • L2 Redis缓存:30分钟
    • 缓存Key:price:sku:{sku_id}:promo:{promo_id}
  • 性能指标:P99 < 100ms
  • 设计理由:PDP场景QPS最高,用户还未选择优惠券,无需计算费用层

场景2:结算试算(Checkout Calculate)

  • 计算范围:基础价格 + 营销促销(不含支付渠道费)
  • 缓存策略
    • 可使用快照数据(5分钟有效期,见ADR-008)
    • 库存必须实时查询(不缓存)
  • 性能指标:P95 < 300ms
  • 设计理由:试算阶段性能优先,允许使用快照提升性能

场景3:支付前试算(Payment Calculate)

  • 计算范围:全4层(基础+营销+费用+优惠券)
  • 缓存策略:不缓存,每次实时计算
  • 性能指标:P95 < 200ms(防抖100ms)
  • 设计理由:用户选择优惠券/支付渠道时实时反馈,必须准确

性能优化技巧

  1. 批量接口优化

    1
    2
    3
    4
    5
    6
    7
    // 坏的实践:循环调用单个接口
    for _, item := range items {
    price := pricingClient.Calculate(item) // N次RPC
    }

    // 好的实践:批量接口
    prices := pricingClient.BatchCalculate(items) // 1次RPC
  2. 并发调用无依赖服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 并发调用Product + Inventory
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
    products = productClient.BatchGet(skuIDs)
    wg.Done()
    }()
    go func() {
    stocks = inventoryClient.BatchCheck(skuIDs)
    wg.Done()
    }()
    wg.Wait()

    // 串行调用有依赖的服务
    promos = marketingClient.GetPromotions(skuIDs) // 依赖skuIDs
    prices = pricingClient.Calculate(products, promos) // 依赖products + promos
  3. 缓存预热

  • 大促前提前预热热门商品价格缓存
  • 营销活动生效前批量计算并写入Redis
  1. 降级策略
  • Marketing Service失败 → 返回base_price,不展示促销
  • Pricing Service失败 → 标记”价格加载中”

追问方向

  1. 如何确定缓存TTL?

    • 基于数据变化频率:base_price(天级)> promotion(分钟级)
    • 基于业务容忍度:价格展示允许5分钟延迟,创单必须实时
    • 通过监控调整:缓存命中率、数据更新频率
  2. 如果缓存击穿(热key失效)怎么办?

    • 使用互斥锁(singleflight)
    • 缓存永不过期 + 异步更新
    • 多级缓存兜底(L1本地缓存)
  3. 大促期间QPS激增如何应对?

    • HPA自动扩容(CPU>70%触发)
    • 降级非核心功能(推荐、评价)
    • 限流保护(API Gateway层)

答题要点

  • 分场景差异化
  • 缓存分层策略
  • 批量接口+并发调用
  • 降级保护

加分项

  • 提及具体性能指标(P99、命中率)
  • 提及监控埋点与调优经验
  • 提及大促保障经验

Q3:跨商品促销如何计算?优先级如何处理?

考察点:复杂业务逻辑实现、算法设计、边界条件处理

参考答案

跨商品促销是电商计价中最复杂的场景,涉及多个商品、多个促销活动的组合计算。

促销活动分类(4级优先级)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Level 1: 商品级促销(Item-Level)
├─ 单品折扣:某个SKU享受9折
├─ 限时购:特价促销
└─ 买N送M:买2送1

Level 2: 品类级促销(Category-Level)
├─ 品类折扣:配件类8折
└─ 品类满减:数码类满200减20

Level 3: 订单级促销(Order-Level)
├─ 满减:满300减50
├─ 满折:满500打9折
└─ 阶梯折扣:满1000打8折

Level 4: 优惠券级(Coupon-Level)
├─ 满减券:满500减50
├─ 折扣券:全场9折
└─ 品类券:数码类专用券

计算顺序(从上到下依次应用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
func (s *PricingService) CalculateFinalPrice(items []*Item, promos []*Promotion) *PriceDetail {
// 1. 计算商品原价
subtotal := calculateSubtotal(items) // 299*2 + 89*1 + 19*3 = 744

// 2. 应用商品级别促销(每个SKU独立计算)
itemDiscount := 0.0
for _, item := range items {
if promo := findItemPromo(item.SkuID, promos); promo != nil {
discount := item.Price * item.Quantity * (1 - promo.DiscountRate)
itemDiscount += discount
}
}
// 示例:SKU 1001(299*2)打9折 = 598*0.9 = 538.2,优惠59.8

// 3. 应用品类级别促销(按品类分组计算)
categoryDiscount := 0.0
itemsByCategory := groupByCategory(items)
for category, categoryItems := range itemsByCategory {
if promo := findCategoryPromo(category, promos); promo != nil {
categorySubtotal := sum(categoryItems)
if categorySubtotal >= promo.Threshold && len(categoryItems) >= promo.MinQuantity {
discount := categorySubtotal * (1 - promo.DiscountRate)
categoryDiscount += discount
}
}
}
// 示例:配件类3件(57元)买3件8折 = 57*0.8 = 45.6,优惠11.4

// 4. 应用订单级别促销(全局计算)
afterItemAndCategory := subtotal - itemDiscount - categoryDiscount // 672.8
orderDiscount := 0.0
for _, promo := range findOrderPromos(promos) {
if afterItemAndCategory >= promo.Threshold {
orderDiscount += promo.ReduceAmount
}
}
// 示例:满300减50 = 50

// 5. 应用优惠券(最后应用,避免券叠加问题)
afterOrder := afterItemAndCategory - orderDiscount // 622.8
couponDiscount := 0.0
if coupon := findApplicableCoupon(promos); coupon != nil {
if afterOrder >= coupon.Threshold {
couponDiscount = coupon.ReduceAmount
}
}
// 示例:满500减50 = 50

// 6. 最终总价
total := afterOrder - couponDiscount // 572.8

return &PriceDetail{
Subtotal: subtotal, // 744.00
ItemDiscount: itemDiscount, // 59.80
CategoryDiscount: categoryDiscount, // 11.40
OrderDiscount: orderDiscount, // 50.00
CouponDiscount: couponDiscount, // 50.00
Total: total, // 572.80
Saved: subtotal - total, // 171.20
}
}

互斥与叠加规则

  1. 同级互斥:同一级别的促销活动默认互斥,取优惠金额最大的

    1
    2
    3
    // 示例:某商品同时参与"9折"和"限时8折"
    promos := findItemPromos(skuID)
    bestPromo := selectMaxDiscount(promos) // 选择8折
  2. 跨级叠加:不同级别的促销可以叠加

    1
    2
    // 商品9折 + 订单满减 + 优惠券
    finalPrice = basePrice * 0.9 - orderReduce - couponReduce
  3. 特殊互斥:通过配置控制

    1
    2
    3
    4
    5
    type Promotion struct {
    ID string
    ExcludeWith []string // 互斥的促销ID列表
    MustUseAlone bool // 是否必须独享(不可与其他促销叠加)
    }

追问方向

  1. 如果用户选了3个商品,涉及2个品类促销+1个订单满减+1张优惠券,计算顺序是什么?

    • 按照4级优先级:商品级 → 品类级 → 订单级 → 优惠券级
    • 每一级计算后更新”当前金额”,作为下一级的输入
  2. 如何避免用户通过多次试算找到最优组合(性能问题)?

    • 前端防抖(100ms)
    • 用户维度限流(10次/分钟)
    • 结果缓存(相同输入5分钟内返回缓存)
  3. 促销规则如何配置?支持运营自定义吗?

    • 运营后台配置(低代码配置平台)
    • 规则引擎解释执行(避免代码发布)
    • 支持规则模拟测试(沙箱环境)
  4. 如何测试促销计算的正确性?

    • 单元测试:覆盖所有促销类型和组合
    • 基准测试:与老系统空跑比对
    • 灰度验证:线上1%流量验证差异率

答题要点

  • 4级优先级分类
  • 依次计算、逐层扣减
  • 互斥与叠加规则
  • 边界条件处理

加分项

  • 提及规则引擎设计
  • 提及灰度验证经验
  • 提及性能优化(缓存、限流)
  • 提及监控指标(计算耗时、差异率)

常见误区

  • ❌ 无法清晰说明计算顺序
  • ❌ 忽略互斥规则
  • ❌ 忽略性能问题(用户多次试算)

Q4:价格计算的精度如何处理?如何避免浮点误差?

考察点:工程细节、数值计算、边界条件处理

参考答案

价格计算涉及金额,精度问题非常关键,浮点数计算会导致精度丢失,必须使用整数计算。

核心原则:全部用分(int64)存储和计算

1
2
3
4
5
6
7
8
9
10
// ❌ 错误做法:使用float64
type Price struct {
Amount float64 // 199.99元
}
// 问题:0.1 + 0.2 != 0.3(浮点误差)

// ✅ 正确做法:使用int64(以分为单位)
type Price struct {
AmountInCents int64 // 19999分(199.99元)
}

多币种精度处理

不同币种的小数位数不同,需要按币种精度表对齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var CurrencyPrecision = map[string]int{
"CNY": 2, // 人民币:2位小数(分)
"USD": 2, // 美元:2位小数(美分)
"JPY": 0, // 日元:0位小数(无零钱)
"VND": 0, // 越南盾:0位小数
"THB": 2, // 泰铢:2位小数
}

// 金额存储:统一用最小单位(分/美分/日元)
type Money struct {
Amount int64 // 金额(最小单位)
Currency string // 币种
}

// 显示转换
func (m *Money) Display() string {
precision := CurrencyPrecision[m.Currency]
divisor := math.Pow10(precision)
displayAmount := float64(m.Amount) / divisor
return fmt.Sprintf("%."+strconv.Itoa(precision)+"f %s", displayAmount, m.Currency)
}

舍入规则:银行家舍入法

1
2
3
4
5
6
7
8
9
10
11
// 银行家舍入(四舍六入五取偶)
func BankersRound(value float64, precision int) int64 {
shift := math.Pow10(precision)
rounded := math.Round(value * shift)
return int64(rounded)
}

// 示例
BankersRound(2.5, 0) // 2(5前面是偶数2,向下)
BankersRound(3.5, 0) // 4(5前面是奇数3,向上)
BankersRound(2.135, 2) // 2.14(5后面还有数字,向上)

分摊场景:余额递减法

当优惠券需要分摊到多个商品时,使用”余额递减法”避免尾差:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 场景:3个商品分摊50元优惠券
// 商品1: 299元(权重299/744)
// 商品2: 89元(权重89/744)
// 商品3: 356元(权重356/744)

func AllocateDiscount(items []*Item, totalDiscount int64) map[int64]int64 {
totalPrice := sum(items)
remaining := totalDiscount
result := make(map[int64]int64)

// 前N-1项:按权重向下取整
for i := 0; i < len(items)-1; i++ {
allocated := (items[i].Price * totalDiscount) / totalPrice
result[items[i].SkuID] = allocated
remaining -= allocated
}

// 最后一项:承担所有尾差
lastItem := items[len(items)-1]
result[lastItem.SkuID] = remaining

return result
}

// 示例结果
// 商品1: floor(299/744 * 5000) = 2009分(20.09元)
// 商品2: floor(89/744 * 5000) = 597分(5.97元)
// 商品3: 5000 - 2009 - 597 = 2394分(23.94元)✓ 总和=5000

促销折扣的精度处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 场景:商品原价299元,促销85折
originalPrice := int64(29900) // 299.00元(分)
discountRate := 0.85

// ❌ 错误:直接乘浮点数
discountedPrice := int64(float64(originalPrice) * discountRate)
// 结果:25415分,可能有误差

// ✅ 正确:乘整数比例,再除以100
discountedPrice := (originalPrice * 85) / 100
// 结果:(29900 * 85) / 100 = 25415分(254.15元)

// 如果需要四舍五入
discountedPrice := (originalPrice * 85 + 50) / 100 // +50实现四舍五入

追问方向

  1. 为什么不用decimal类型?

    • decimal库性能较差(比int64慢10倍)
    • 增加依赖库,影响编译和部署
    • int64足够表示金额范围(922万亿分,足够使用)
  2. 如果促销折扣是85折,计算后有小数如何处理?

    • 使用银行家舍入法
    • 或者配置舍入策略(向上/向下/四舍五入)
    • 舍入规则需要在合同中明确(法律合规)
  3. 跨币种场景(如美元+人民币)如何处理?

    • 统一转换为基准币种(如USD)
    • 使用实时汇率 + 汇率缓存(5分钟更新)
    • 存储原始币种金额 + 汇率 + 转换后金额(审计需要)
  4. 如何保证分摊后总和等于原始金额?

    • 使用余额递减法(最后一项承担尾差)
    • 单元测试验证:sum(allocated) == totalDiscount

答题要点

  • int64存储(以分为单位)
  • 银行家舍入法
  • 余额递减法(分摊场景)
  • 多币种精度表

加分项

  • 提及法律合规要求
  • 提及审计追溯需求
  • 提及性能对比(int64 vs decimal)
  • 提及单元测试覆盖

常见误区

  • ❌ 使用float64或float32
  • ❌ 不了解银行家舍入法
  • ❌ 分摊算法导致总和不等

1.2 营销规则引擎

Q5:营销活动的优先级如何设计?如何处理冲突?

考察点:规则引擎设计、冲突处理、配置化能力

参考答案

营销活动的优先级处理是规则引擎的核心,直接影响用户体验和资金安全。

优先级维度设计(3个维度)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Promotion struct {
ID string
Type PromotionType // 促销类型
Level PromotionLevel // 促销层级(商品/品类/订单/优惠券)
Priority int // 优先级(数字越小越优先)
ExclusiveTag string // 互斥标签(相同标签的促销互斥)
Stackable bool // 是否可叠加
StartTime time.Time
EndTime time.Time
}

type PromotionLevel int
const (
LevelItem PromotionLevel = 1 // 商品级(优先级最高)
LevelCategory PromotionLevel = 2 // 品类级
LevelOrder PromotionLevel = 3 // 订单级
LevelCoupon PromotionLevel = 4 // 优惠券级(优先级最低)
)

冲突处理策略

  1. 按Level分组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    func (e *PricingEngine) SelectPromotions(allPromos []*Promotion) []*Promotion {
    // Step 1: 按Level分组
    promosByLevel := groupByLevel(allPromos)

    selected := make([]*Promotion, 0)

    // Step 2: 每个Level内部处理冲突
    for level := LevelItem; level <= LevelCoupon; level++ {
    levelPromos := promosByLevel[level]
    resolvedPromos := e.resolveConflicts(levelPromos)
    selected = append(selected, resolvedPromos...)
    }

    return selected
    }
  2. 同Level内冲突解决

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    func (e *PricingEngine) resolveConflicts(promos []*Promotion) []*Promotion {
    // 按互斥标签分组
    promosByTag := make(map[string][]*Promotion)
    stackablePromos := make([]*Promotion, 0)

    for _, promo := range promos {
    if promo.Stackable {
    stackablePromos = append(stackablePromos, promo)
    } else if promo.ExclusiveTag != "" {
    promosByTag[promo.ExclusiveTag] = append(promosByTag[promo.ExclusiveTag], promo)
    }
    }

    result := make([]*Promotion, 0)

    // 每个互斥组选择一个最优促销
    for _, tagPromos := range promosByTag {
    bestPromo := selectBestPromo(tagPromos) // 按优先级或优惠金额
    result = append(result, bestPromo)
    }

    // 可叠加促销全部生效
    result = append(result, stackablePromos...)

    return result
    }
  3. 最优促销选择策略

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    func selectBestPromo(promos []*Promotion) *Promotion {
    if len(promos) == 0 {
    return nil
    }

    // 策略1:按配置的优先级(Priority字段)
    sort.Slice(promos, func(i, j int) bool {
    return promos[i].Priority < promos[j].Priority
    })

    // 策略2:如果优先级相同,计算实际优惠金额,取最大
    maxPromo := promos[0]
    maxDiscount := calculateDiscount(maxPromo)

    for _, promo := range promos[1:] {
    if promo.Priority == maxPromo.Priority {
    discount := calculateDiscount(promo)
    if discount > maxDiscount {
    maxDiscount = discount
    maxPromo = promo
    }
    }
    }

    return maxPromo
    }

实际案例

1
2
3
4
5
6
7
8
9
10
11
12
13
场景:某商品同时参与3个促销活动

促销A:单品9折(Level=Item, Priority=10, ExclusiveTag="discount", Stackable=false)
促销B:单品限时85折(Level=Item, Priority=5, ExclusiveTag="discount", Stackable=false)
促销C:全场满300减50(Level=Order, Priority=20, ExclusiveTag="", Stackable=true)

处理流程:
1. 按Level分组:[A, B] 属于LevelItem,[C] 属于LevelOrder
2. LevelItem内冲突解决:
- A和B有相同ExclusiveTag="discount",互斥
- 比较Priority:B(5) < A(10),选择B
3. LevelOrder:C可叠加,直接生效
4. 最终生效:B(85折)+ C(满300减50)

配置化设计

运营后台可配置促销规则,无需代码发布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"promotion_id": "PROMO_001",
"name": "双11大促",
"type": "discount",
"level": "item",
"priority": 10,
"exclusive_tag": "anniversary",
"stackable": false,
"conditions": {
"sku_ids": [1001, 1002],
"min_quantity": 1
},
"discount": {
"type": "rate",
"value": 0.85
},
"valid_time": {
"start": "2026-11-11 00:00:00",
"end": "2026-11-11 23:59:59"
}
}

追问方向

  1. 如何支持”同时享受最多3个促销”这种限制?

    • 在resolveConflicts中增加数量限制
    • 按优惠金额排序,取Top 3
  2. 如果促销活动在用户试算和创单之间变更,如何保证用户不吃亏?

    • 试算生成快照ID,记录当时的促销规则
    • 创单时重新校验促销有效性
    • 如果促销失效但对用户更优,仍使用快照价格(ADR-008)
  3. 大促期间(如双11)促销活动特别多,如何保证性能?

    • 促销规则缓存(Redis,5分钟TTL)
    • 提前预计算(活动生效前批量计算并缓存)
    • 限制单次查询的促销数量(最多50个)
  4. 如何防止促销活动配置错误导致资损?

    • 促销规则审批流程(运营→审核→上线)
    • 沙箱环境模拟测试
    • 灰度发布(先1%流量验证)
    • 资损监控(优惠金额异常告警)

答题要点

  • 3维度优先级(Level、Priority、优惠金额)
  • 互斥与叠加规则
  • 配置化规则引擎
  • 防御性设计

加分项

  • 提及规则引擎框架(Drools、自研)
  • 提及灰度发布经验
  • 提及资损防控措施
  • 提及性能优化(缓存、预计算)

Q6:如何保证营销活动的实时性?缓存如何刷新?

考察点:缓存一致性、事件驱动、实时性保障

参考答案

营销活动的实时性要求很高,特别是限时促销(如秒杀、闪购),必须在活动生效/失效时立即生效。

多级缓存架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────┐
│ L1: 本地缓存(Application Level) │
│ • TTL: 1分钟(极短) │
│ • 命中率: 60% │
│ • 更新方式: 被动过期 │
│ • 适用: 变化不频繁的促销 │
└─────────────────────────────────────────┘
↓ Miss
┌─────────────────────────────────────────┐
│ L2: Redis缓存(Distributed Level) │
│ • TTL: 5分钟 │
│ • 命中率: 95% │
│ • 更新方式: 主动推送 + 被动过期 │
│ • Key: promo:sku:{sku_id} │
└─────────────────────────────────────────┘
↓ Miss
┌─────────────────────────────────────────┐
│ L3: MySQL(Source of Truth) │
│ • 权威数据源 │
│ • 实时查询 │
└─────────────────────────────────────────┘

缓存刷新策略(3种方式)

方式1:被动过期(Lazy Expiration)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *MarketingService) GetPromotions(skuID int64) ([]*Promotion, error) {
// Step 1: 查询Redis
cacheKey := fmt.Sprintf("promo:sku:%d", skuID)
if cached, err := s.redis.Get(cacheKey); err == nil {
return deserialize(cached), nil
}

// Step 2: 缓存未命中,查询MySQL
promos, err := s.repo.FindBySkuID(skuID)
if err != nil {
return nil, err
}

// Step 3: 写入Redis(5分钟TTL)
s.redis.Set(cacheKey, serialize(promos), 5*time.Minute)

return promos, nil
}
  • 优点:实现简单,无需额外基础设施
  • 缺点:首次访问慢(缓存未命中),TTL内数据可能陈旧

方式2:主动推送(Event-Driven)

通过Kafka事件驱动主动刷新缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 促销活动变更时发布事件
func (s *MarketingService) UpdatePromotion(promo *Promotion) error {
// Step 1: 更新MySQL
if err := s.repo.Update(promo); err != nil {
return err
}

// Step 2: 删除Redis缓存(让其自然过期重建)
s.redis.Del(fmt.Sprintf("promo:sku:%d", promo.SkuID))

// Step 3: 发布Kafka事件
event := &PromotionUpdatedEvent{
PromoID: promo.ID,
SkuID: promo.SkuID,
Action: "update",
Timestamp: time.Now(),
}
s.kafka.Publish("promotion.updated", event)

return nil
}

// 订阅者监听事件并刷新缓存
func (s *PricingService) HandlePromotionUpdated(event *PromotionUpdatedEvent) {
// 删除本地缓存
s.localCache.Del(fmt.Sprintf("promo:sku:%d", event.SkuID))

// 预热Redis缓存(可选)
promos, _ := s.marketingClient.GetPromotions(event.SkuID)
s.redis.Set(fmt.Sprintf("promo:sku:%d", event.SkuID), serialize(promos), 5*time.Minute)
}
  • 优点:实时性强,缓存几乎立即生效
  • 缺点:需要Kafka基础设施,增加复杂度

方式3:定时刷新(Scheduled Refresh)

针对限时促销(如秒杀),提前预热缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定时任务:每分钟扫描即将生效的促销活动
func (job *PromotionPrewarmJob) Run() {
// 查询未来5分钟内生效的促销
now := time.Now()
upcomingPromos := job.repo.FindByTimeRange(now, now.Add(5*time.Minute))

for _, promo := range upcomingPromos {
// 计算到生效时间的延迟
delay := promo.StartTime.Sub(now)

// 定时器:到点刷新缓存
time.AfterFunc(delay, func() {
job.prewarmCache(promo)
})
}
}

func (job *PromotionPrewarmJob) prewarmCache(promo *Promotion) {
// 预热所有相关SKU的缓存
for _, skuID := range promo.SkuIDs {
promos, _ := job.marketingService.GetPromotions(skuID)
cacheKey := fmt.Sprintf("promo:sku:%d", skuID)
job.redis.Set(cacheKey, serialize(promos), 30*time.Minute)
}

log.Info("Prewarmed promotion cache", "promo_id", promo.ID, "sku_count", len(promo.SkuIDs))
}
  • 优点:促销活动生效时缓存已就绪,性能最优
  • 缺点:需要定时任务,占用资源

实时性保障的完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
促销活动生命周期:

T-10min: 定时任务扫描到即将生效的促销

T-5min: 预热缓存(Redis写入)

T0: 促销活动生效
├─ Kafka发布 promotion.activated 事件
├─ 所有Pricing Service实例删除本地缓存
└─ 用户首次访问时从Redis获取最新促销

T+30min: 促销活动变更(如库存不足,提前结束)
├─ MySQL更新状态
├─ Redis删除缓存
├─ Kafka发布 promotion.deactivated 事件
└─ 用户下次访问时获取最新状态

监控指标

1
2
3
4
5
6
7
// 营销活动实时性监控
type PromotionMetrics struct {
CacheHitRate float64 // 缓存命中率(目标>90%)
CacheSyncDelay int64 // 缓存同步延迟(目标<3秒)
InvalidPromoCalls int64 // 失效促销被调用次数(目标<100/分钟)
EventPublishDelay int64 // 事件发布延迟(目标<1秒)
}

追问方向

  1. 如果营销规则更新,如何快速刷新缓存?

    • 主动删除Redis缓存(Delete操作)
    • Kafka事件通知所有服务实例
    • 下次访问时自动重建缓存
  2. 大促期间(如双11)营销活动特别多,如何保证性能?

    • 提前1天预热所有促销缓存
    • 缓存TTL延长到30分钟(活动期间不频繁变更)
    • Redis Cluster扩容(8主8从 → 16主16从)
  3. 如何处理缓存雪崩(大量促销同时失效)?

    • TTL加随机偏移(5分钟±30秒)
    • 使用互斥锁(singleflight)防止缓存击穿
    • 多级缓存兜底(本地缓存)
  4. 如何监控缓存一致性?

    • 采样对比:定时采样100个SKU,对比Redis和MySQL
    • 一致性告警:不一致率>1%触发告警
    • 自动修复:发现不一致时自动刷新缓存

答题要点

  • 多级缓存架构
  • 3种刷新方式(被动过期、主动推送、定时预热)
  • 事件驱动(Kafka)
  • 监控指标

加分项

  • 提及具体性能指标
  • 提及大促保障经验
  • 提及缓存一致性验证
  • 提及雪崩/击穿防护

Q7:如何进行价格计算的灰度迁移?如何保证0资损?

考察点:灰度发布策略、安全迁移、风险控制

参考答案

价格计算涉及资金,灰度迁移必须极其谨慎。我们采用”三阶段灰度 + 空跑比对”策略,实现了10+品类的0资损安全迁移。

迁移背景

  • 老系统:价格逻辑分散在5+个服务中(前端、订单、支付、营销等)
  • 新系统:统一价格计算引擎(四层计价模型)
  • 风险:计算差异导致资损(老系统年均3-5次资损事故)
  • 目标:0资损、平滑迁移、可快速回滚

三阶段灰度策略

阶段1:空跑阶段(2周,0%线上流量)

新老系统并行运行,新系统结果不返回给用户,只做比对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
// 老系统计算(主流程,返回给用户)
oldResult, err := s.oldPricingService.Calculate(ctx, req)
if err != nil {
return nil, err
}

// 新系统计算(异步,不阻塞主流程,仅用于比对)
go func() {
defer func() {
if r := recover(); r != nil {
s.recordError("new_pricing_panic", r)
}
}()

newResult, err := s.newPricingService.Calculate(context.Background(), req)
if err != nil {
s.recordError("new_pricing_error", err)
return
}

// 比对差异
diff := s.comparePriceResults(oldResult, newResult, req)

// 记录差异(100%采样)
s.metrics.RecordDifference(diff)

if diff.HasDifference() {
// 差异上报到监控系统
s.reportDifference(diff)

// 差异>10%记录详细日志
if diff.DiffRate > 0.10 {
s.logger.Error("price difference too large",
"order_id", req.OrderID,
"old_price", oldResult.FinalPrice,
"new_price", newResult.FinalPrice,
"diff_rate", diff.DiffRate,
"request", req)
}
}
}()

// 返回老系统结果(用户无感知)
return oldResult, nil
}

type PriceDifference struct {
OrderID string
OldFinalPrice float64
NewFinalPrice float64
Difference float64 // 绝对差异
DiffRate float64 // 差异率(百分比)
Layer string // 哪一层有差异(base/promo/fee/coupon)
Category string // 品类
Timestamp int64
}

空跑阶段监控大盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type DryRunDashboard struct {
// 基础统计
TotalSamples int64 // 总样本数(2周约280万单)
DifferenceCount int64 // 差异数量
DifferenceRate float64 // 差异率(目标<0.1%)

// 差异金额统计
AvgDiffAmount float64 // 平均差异金额
MaxDiffAmount float64 // 最大差异金额
P99DiffAmount float64 // P99差异金额

// 分层差异统计
BasePriceDiff int64 // 基础价格层差异数
PromoDiff int64 // 营销层差异数
FeeDiff int64 // 费用层差异数
CouponDiff int64 // 优惠券层差异数

// 分品类差异统计
DiffByCategory map[string]int64 // {"flight": 120, "hotel": 85, ...}

// Top差异订单
TopDiffOrders []*PriceDifference // Top 100差异订单,人工审查
}

空跑阶段发现的典型问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
问题1:促销互斥规则不一致
- 老系统:商品折扣和满减可叠加
- 新系统:默认互斥(配置错误)
- 影响:5%订单价格差异
- 修复:调整新系统互斥规则配置

问题2:精度舍入差异
- 老系统:四舍五入
- 新系统:银行家舍入
- 影响:0.5%订单价格差异±0.01元
- 决策:统一为银行家舍入(更标准)

问题3:分摊算法差异
- 老系统:平均分摊(有尾差)
- 新系统:余额递减法
- 影响:多商品订单差异±0.02元
- 决策:新系统更准确,保留

问题4:优惠券叠加边界条件
- 老系统:满减券与折扣券可叠加
- 新系统:只支持一张券
- 影响:0.3%订单少了一重优惠
- 修复:新系统支持券叠加(配置化)

阶段1成果

  • 运行2周,100%采样
  • 差异率从初期5.2%降至0.048%
  • 发现并修复15个隐藏问题
  • 生成差异分析报告,供技术评审

阶段2:灰度放量(4周,1%→100%)

新系统开始返回结果给用户,逐步放量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 灰度策略:基于用户ID的哈希值
func (s *CheckoutService) shouldUseNewPricing(userID int64) bool {
// 1. 动态配置灰度比例
grayPercentage := s.configCenter.GetInt("new_pricing_gray_percentage")

// 2. 基于用户ID的一致性哈希
hash := fnv1a(userID)
bucket := hash % 100

// 3. 判断是否在灰度范围内
inGray := bucket < grayPercentage

// 4. 记录灰度命中日志
s.logger.Debug("gray decision",
"user_id", userID,
"bucket", bucket,
"gray_percentage", grayPercentage,
"use_new", inGray)

return inGray
}

// 灰度主流程
func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
if s.shouldUseNewPricing(req.UserID) {
// 使用新系统
result, err := s.newPricingService.Calculate(ctx, req)
if err != nil {
// 新系统失败,自动降级到老系统
s.metrics.RecordDegradation("new_to_old")
s.logger.Warn("new pricing failed, fallback to old", "error", err)
return s.oldPricingService.Calculate(ctx, req)
}
return result, nil
}

// 使用老系统
return s.oldPricingService.Calculate(ctx, req)
}

灰度放量计划(4周)

周次 灰度比例 每日订单量 放量条件 观察期
Week 1 1% 2万单 - 48小时无异常
Week 2 10% 20万单 差异率<0.01%
错误率<0.1%
P99<300ms
48小时无异常
Week 3 50% 100万单 差异率<0.01%
无资损告警
48小时无异常
Week 4 100% 200万单 所有指标正常 持续观察2周

每次放量的检查清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
放量前(T-30min):
✅ 新系统错误率<0.1%
✅ 新系统P99延迟<300ms
✅ 新老系统差异率<0.01%
✅ 无资损告警
✅ 数据库连接池充足
✅ Redis容量充足
✅ 告警规则配置就绪

放量中(T0):
✅ 配置中心修改灰度比例
✅ 实时监控错误率/延迟/差异率
✅ 准备回滚方案(一键设置为0%)

放量后(T+24h):
✅ 观察24小时无异常
✅ 抽查订单样本(人工审核)
✅ 用户投诉率无异常
✅ 决策是否继续放量

自动降级机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 后台监控:新系统错误率过高时自动降级
func (monitor *PricingMonitor) Run() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()

for range ticker.C {
metrics := monitor.collectMetrics()

// 触发条件:错误率>5%
if metrics.NewPricingErrorRate > 0.05 {
// 自动降级到0%
monitor.configCenter.Set("new_pricing_gray_percentage", 0)

// 紧急告警
monitor.alerting.SendUrgentAlert(
"新价格计算系统自动降级",
fmt.Sprintf("错误率%.2f%%超过阈值5%%", metrics.NewPricingErrorRate*100),
"@pricing-team @sre-oncall")

// 记录降级事件
monitor.recordDegradationEvent(metrics)
}

// 触发条件:差异率>1%
if metrics.DifferenceRate > 0.01 {
monitor.alerting.SendAlert(
"价格差异率过高",
fmt.Sprintf("差异率%.2f%%,请检查计算逻辑", metrics.DifferenceRate*100))
}
}
}

快速回滚机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 人工回滚(1分钟内完成)
// 1. 通过配置中心设置灰度比例为0
configCenter.Set("new_pricing_gray_percentage", 0)

// 2. 所有Checkout Service实例自动监听配置变更
func (s *CheckoutService) watchConfig() {
s.configCenter.Watch("new_pricing_gray_percentage", func(oldVal, newVal int) {
s.logger.Info("gray percentage changed",
"old", oldVal,
"new", newVal)

// 无需重启服务,动态生效
s.grayPercentage.Store(newVal)
})
}

// 3. 下一次请求立即使用老系统

阶段3:稳定观察(2周,100%流量)

100%流量切换后,继续保留老系统代码做采样比对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
// 新系统计算(主流程,返回给用户)
newResult, err := s.newPricingService.Calculate(ctx, req)
if err != nil {
return nil, err
}

// 1%采样:继续比对老系统(异步,不阻塞)
if rand.Intn(100) < 1 {
go func() {
oldResult, _ := s.oldPricingService.Calculate(context.Background(), req)
diff := s.comparePriceResults(oldResult, newResult, req)
if diff.HasDifference() {
s.reportDifference(diff)
}
}()
}

return newResult, nil
}

稳定2周后,下线老系统

  • 移除老系统代码
  • 关闭空跑比对
  • 归档灰度期间的数据和报告

追问方向

  1. 如果空跑阶段发现差异率很高(如5%),如何定位问题?

    • 按差异层级分类统计(base/promo/fee/coupon)
    • 按品类分类统计(机票/酒店/充值)
    • 抽样Top 100差异订单,人工对比计算明细
    • 单元测试覆盖边界条件
    • 数据驱动的回归测试
  2. 灰度期间如何保证同一用户体验一致?

    • 按用户ID哈希,同一用户始终路由到相同系统
    • 避免用户A看到价格X,刷新后变成价格Y(体验极差)
    • 灰度比例调整时仍保持用户粘性
  3. 如果灰度过程中发现问题,如何快速回滚?

    • 动态配置中心:将new_pricing_gray_percentage设为0
    • 所有服务实例监听配置变更,立即生效(无需重启)
    • 回滚时间<1分钟
    • 回滚后继续监控,确保无二次故障
  4. 如何验证灰度的有效性?

    • 技术指标:错误率、延迟、差异率
    • 业务指标:订单转化率、用户投诉率、退款率
    • 资金安全:资损事故次数(目标:0次)
    • A/B测试:新老系统的GMV对比

答题要点

  • 三阶段灰度(空跑、放量、稳定)
  • 空跑100%采样比对
  • 基于用户ID的一致性哈希
  • 自动降级+快速回滚
  • 多维度监控指标

加分项

  • 提及具体差异率指标(0.048%)
  • 提及发现的问题数量(15个)
  • 提及灰度放量时间表
  • 提及自动降级机制
  • 提及A/B测试验证

常见误区

  • ❌ 直接全量上线(风险极高)
  • ❌ 没有空跑阶段(无法提前发现问题)
  • ❌ 没有自动降级机制(出问题依赖人工)
  • ❌ 灰度期间用户体验不一致

1.3 价格场景化设计

Q8:为什么支付页面需要实时试算?如何设计这个接口?

考察点:用户体验设计、实时计算、性能优化

参考答案

支付页面的实时试算(ADR-010 Phase 3a)是”先创单后支付”模式的关键环节,直接影响用户体验和支付转化率。

为什么需要实时试算?

用户行为分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
创建订单(订单金额538元)

进入支付页面

用户操作1:选择优惠券"满500减50"
→ 问题:最终要付多少钱?488元还是487.50元?

用户操作2:选择使用50个Coin抵扣
→ 问题:Coin抵扣后金额是多少?

用户操作3:选择花呗分期3期
→ 问题:分期手续费是多少?最终多少钱?

如果没有实时试算:
❌ 用户不知道最终支付金额
❌ 用户不知道哪个优惠券最划算
❌ 用户点击"确认支付"后才发现金额不对
❌ 支付转化率下降(用户疑惑、不信任)

实时试算接口设计(文档4.5-Phase 3a):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// POST /payment/calculate - 支付前试算
type PaymentCalculateRequest struct {
OrderID int64 `json:"order_id" binding:"required"`
CouponID string `json:"coupon_id"` // 用户选择的优惠券
CoinAmount int64 `json:"coin_amount"` // 使用的Coin数量
PaymentChannel string `json:"payment_channel"` // 支付渠道
}

type PaymentCalculateResponse struct {
OrderID int64 `json:"order_id"`
BaseAmount float64 `json:"base_amount"` // 订单基础金额
CouponDiscount float64 `json:"coupon_discount"` // 优惠券抵扣
CoinDiscount float64 `json:"coin_discount"` // Coin抵扣
ChannelFee float64 `json:"channel_fee"` // 支付渠道费
FinalAmount float64 `json:"final_amount"` // 最终支付金额
Breakdown *PriceBreakdown `json:"breakdown"` // 详细明细
RemainingCoin int64 `json:"remaining_coin"` // 使用后剩余Coin
}

func (s *PaymentService) Calculate(ctx context.Context, req *PaymentCalculateRequest) (*PaymentCalculateResponse, error) {
// Step 1: 查询订单基础金额
order, err := s.orderClient.GetOrder(ctx, req.OrderID)
if err != nil {
return nil, fmt.Errorf("订单不存在: %w", err)
}

// 校验订单状态(必须是待支付)
if order.Status != OrderStatusPendingPayment {
return nil, fmt.Errorf("订单状态不正确: %s", order.Status)
}

// 校验订单是否过期
if time.Now().After(order.PayExpireAt) {
return nil, fmt.Errorf("订单已过期,请重新下单")
}

baseAmount := order.AmountToPay // 538.00(创单时计算的金额)

// Step 2: 校验并计算优惠券抵扣
couponDiscount := 0.0
if req.CouponID != "" {
coupon, err := s.marketingClient.ValidateCoupon(ctx, req.CouponID, order.UserID)
if err != nil {
// 优惠券无效,返回友好错误
return nil, fmt.Errorf("优惠券无效: %w", err)
}

// 判断是否满足使用条件
if baseAmount >= coupon.Threshold {
couponDiscount = coupon.Amount // 50.00
} else {
return nil, fmt.Errorf("订单金额不满足优惠券使用条件(需满%.2f元)", coupon.Threshold)
}
}

// Step 3: 校验并计算Coin抵扣
coinDiscount := 0.0
if req.CoinAmount > 0 {
userCoins, err := s.marketingClient.GetUserCoins(ctx, order.UserID)
if err != nil {
return nil, err
}

// 校验Coin余额是否足够
if req.CoinAmount > userCoins.Available {
return nil, fmt.Errorf("Coin余额不足,可用:%d,请求:%d",
userCoins.Available, req.CoinAmount)
}

// 计算Coin抵扣金额(1 Coin = 0.01元)
coinDiscount = float64(req.CoinAmount) * 0.01 // 50 * 0.01 = 0.50
}

// Step 4: 计算支付渠道费
afterDiscount := baseAmount - couponDiscount - coinDiscount
channelFee := s.calculateChannelFee(req.PaymentChannel, afterDiscount)

// Step 5: 计算最终支付金额
finalAmount := afterDiscount + channelFee

// Step 6: 构建响应
return &PaymentCalculateResponse{
OrderID: req.OrderID,
BaseAmount: baseAmount, // 538.00
CouponDiscount: couponDiscount, // -50.00
CoinDiscount: coinDiscount, // -0.50
ChannelFee: channelFee, // +0.00
FinalAmount: finalAmount, // 487.50
Breakdown: &PriceBreakdown{
Items: []PriceItem{
{"订单金额", baseAmount},
{"优惠券", -couponDiscount},
{"Coin抵扣", -coinDiscount},
{"渠道费", channelFee},
},
Total: finalAmount,
},
RemainingCoin: userCoins.Available - req.CoinAmount,
}, nil
}

// 计算支付渠道费
func (s *PaymentService) calculateChannelFee(channel string, amount float64) float64 {
feeRates := map[string]float64{
"alipay": 0.000, // 支付宝无手续费
"wechat": 0.000, // 微信无手续费
"card": 0.010, // 信用卡1%
"huabei_3": 0.020, // 花呗分期3期2%
"huabei_6": 0.040, // 花呗分期6期4%
}

rate, ok := feeRates[channel]
if !ok {
rate = 0.0 // 默认无手续费
}

return amount * rate
}

前端交互设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 前端:防抖优化(用户快速切换优惠券时)
let calculateTimer = null;

function onCouponChange(couponId) {
// 清除之前的定时器
clearTimeout(calculateTimer);

// 100ms防抖
calculateTimer = setTimeout(() => {
recalculatePayment(couponId);
}, 100);
}

function recalculatePayment(couponId) {
// 显示加载状态
showLoading();

fetch('/payment/calculate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
order_id: currentOrderId,
coupon_id: couponId,
coin_amount: selectedCoinAmount,
payment_channel: selectedChannel
})
})
.then(res => res.json())
.then(data => {
// 更新页面显示(动画效果)
animateAmountChange(currentAmount, data.final_amount);

// 更新明细
updateBreakdown(data.breakdown);

// 隐藏加载状态
hideLoading();
})
.catch(err => {
// 错误处理:显示基础金额
showError("价格计算失败,请稍后重试");
});
}

性能优化措施

优化1:前端防抖

  • 用户快速切换优惠券时,100ms内只发送1次请求
  • 减少90%无效请求

优化2:后端短期缓存

1
2
3
4
5
6
7
8
9
10
11
// 相同输入30秒内返回缓存
cacheKey := fmt.Sprintf("payment:calc:%d:%s:%d:%s",
req.OrderID, req.CouponID, req.CoinAmount, req.PaymentChannel)

if cached, err := s.redis.Get(cacheKey); err == nil {
return deserialize(cached), nil
}

// 计算并缓存
result := s.calculateInternal(ctx, req)
s.redis.Set(cacheKey, serialize(result), 30*time.Second)

优化3:并发RPC调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 并发查询订单、优惠券、Coin(3个独立查询)
var wg sync.WaitGroup
wg.Add(3)

go func() {
order = s.orderClient.GetOrder(ctx, req.OrderID)
wg.Done()
}()

go func() {
if req.CouponID != "" {
coupon = s.marketingClient.ValidateCoupon(ctx, req.CouponID, order.UserID)
}
wg.Done()
}()

go func() {
if req.CoinAmount > 0 {
coins = s.marketingClient.GetUserCoins(ctx, order.UserID)
}
wg.Done()
}()

wg.Wait()

// 总耗时:max(50ms, 30ms, 30ms) = 50ms(vs 串行110ms)

优化4:用户维度限流

1
2
3
4
5
// 防止恶意刷接口
rateLimitKey := fmt.Sprintf("ratelimit:payment:calc:%d", order.UserID)
if !s.rateLimiter.Allow(rateLimitKey, 20, time.Minute) {
return nil, fmt.Errorf("请求过于频繁")
}

性能指标

  • P50延迟:< 50ms(缓存命中)
  • P95延迟:< 150ms(实时计算)
  • P99延迟:< 250ms
  • QPS:1500(正常)/ 8000(大促)
  • 缓存命中率:> 40%(用户反复调整)

追问方向

  1. 为什么不在创单时就计算支付渠道费?

    • 创单时用户还没选择支付渠道
    • 不同渠道费率差异大(0%-4%)
    • 支付页面才能让用户看到渠道费,透明化
  2. 如果用户频繁切换优惠券(每秒10次),如何防止接口被刷爆?

    • 前端防抖100ms(10次变1次)
    • 后端用户限流(20次/分钟)
    • 短期缓存30秒
    • IP限流(防爬虫)
  3. 如果试算接口失败,用户体验如何保障?

    • 降级策略:显示订单基础金额,标记”最终金额支付时确定”
    • 重试机制:前端自动重试1次
    • 用户点击”确认支付”时后端重新计算(兜底)
  4. 如何防止前端篡改试算金额?

    • 试算接口只返回金额展示给用户
    • 确认支付时后端必须重新计算(Phase 3b)
    • 比对前端传来的expected_amount与后端计算是否一致
    • 差异>0.01元拒绝支付

答题要点

  • 用户体验驱动设计(价格透明化)
  • 前端防抖+后端缓存
  • 并发RPC优化
  • 限流保护+降级策略
  • 防篡改设计

加分项

  • 提及具体性能指标(P95<150ms)
  • 提及前后端配合设计
  • 提及安全防护(防篡改、限流)
  • 提及降级策略

Q9:试算接口与创单接口的价格计算有何差异?

考察点:性能与准确性权衡、分阶段设计、风险控制

参考答案

试算和创单是两个不同阶段,对性能和准确性的要求不同,因此价格计算策略也不同。

核心设计理念

1
2
试算阶段:性能优先,允许使用快照数据
创单阶段:准确性优先,强制实时校验

详细对比表(ADR-008):

维度 试算阶段(Calculate) 创单阶段(CreateOrder)
目的 快速预览价格,提升用户体验 准确锁定价格,防止资损
商品数据 可使用快照(5分钟内有效) ✅ 强制实时查询
营销数据 可使用快照(5分钟内有效) ✅ 强制实时查询 + 活动有效性校验
库存数据 ✅ 必须实时查询(但不扣减) ✅ 必须实时查询 + CAS预占
优惠券 只校验,不扣减 预扣(Reserve)
Coin 只校验,不扣减 预扣(Reserve)
价格计算 基于快照/实时混合数据 ✅ 基于最新实时数据
数据一致性 最终一致性(允许5分钟延迟) 强一致性(实时校验)
性能要求 P95 < 230ms P95 < 500ms(可接受稍慢)
缓存策略 可缓存(快照命中率80%) 不缓存(每次实时)
调用频率 高(用户多次试算) 低(一次性操作)
资损风险 无(仅展示) 高(资源锁定)
安全保障 无需防护 ✅ 多重校验(防止资损)

试算接口代码(使用快照数据):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
var needQuerySKUs []int64
snapshotData := make(map[int64]*SnapshotData)

now := time.Now().Unix()

// Step 1: 判断快照是否过期
for _, item := range req.Items {
if item.Snapshot != nil && item.Snapshot.ExpiresAt > now {
// 快照未过期,直接使用
snapshotData[item.SkuID] = item.Snapshot.Data
} else {
// 快照过期或无快照,需要查询
needQuerySKUs = append(needQuerySKUs, item.SkuID)
}
}

// Step 2: 只查询未命中快照的SKU(性能优化)
var products []*Product
var promos []*Promotion
if len(needQuerySKUs) > 0 {
// 并发调用
products, _ = s.productClient.BatchGetProducts(ctx, needQuerySKUs)
promos, _ = s.marketingClient.BatchGetPromotions(ctx, needQuerySKUs, req.UserID)
}

// Step 3: 合并快照和查询数据
allProducts := s.mergeSnapshotAndQueried(snapshotData, products)

// Step 4: 库存必须实时查询(不使用快照)
stocks, _ := s.inventoryClient.BatchCheckStock(ctx, allSKUs)

// Step 5: 计算价格
prices, _ := s.pricingClient.BatchCalculatePrice(ctx, allProducts)

return &CalculateResponse{
Items: buildItems(allProducts, prices, stocks),
TotalPrice: calculateTotal(prices),
CanCheckout: allInStock(stocks, req.Items),
}, nil
}

创单接口代码(强制实时查询):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, error) {
// 注意:创单时不使用任何快照数据,全部实时查询

// Step 1: 实时查询商品信息(不使用快照)
products, err := s.productClient.BatchGetProducts(ctx, req.SkuIDs)
if err != nil {
return nil, fmt.Errorf("查询商品失败: %w", err)
}

// Step 2: 实时查询营销活动(强制最新数据)
promos, err := s.marketingClient.BatchGetPromotions(ctx, req.SkuIDs, req.UserID)
if err != nil {
return nil, fmt.Errorf("查询营销活动失败: %w", err)
}

// Step 3: 校验营销活动有效性(关键:防止使用过期活动)
for _, promo := range promos {
if !s.validatePromotion(promo) {
return nil, fmt.Errorf("促销活动 %s 已失效", promo.ID)
}
}

// Step 4: 预占库存(CAS操作,防止超卖)
reserved, err := s.inventoryClient.ReserveStock(ctx, req.Items)
if err != nil {
return nil, fmt.Errorf("库存不足: %w", err)
}

// Step 5: 实时计算价格(基于最新营销数据)
price, err := s.pricingClient.CalculateFinalPrice(ctx, products, promos)
if err != nil {
// 回滚库存
s.inventoryClient.ReleaseStock(ctx, reserved)
return nil, fmt.Errorf("价格计算失败: %w", err)
}

// Step 6: 创建订单
order := &Order{
OrderID: s.generateOrderID(),
UserID: req.UserID,
Items: req.Items,
TotalPrice: price.FinalPrice,
Status: OrderStatusPendingPayment,
ReserveIDs: reserved,
}

if err := s.orderRepo.Create(ctx, order); err != nil {
s.inventoryClient.ReleaseStock(ctx, reserved)
return nil, fmt.Errorf("创建订单失败: %w", err)
}

return &CreateOrderResponse{
OrderID: order.OrderID,
TotalPrice: price.FinalPrice,
}, nil
}

为什么创单必须实时校验?

风险场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户在详情页看到促销"满300减50"(生成快照)
↓ 5分钟后
用户点击结算(快照仍有效,使用快照数据)
↓ 试算显示:300 - 50 = 250元
用户点击"提交订单"
↓ 此时促销活动库存已用完(活动限量100份)

如果创单也使用快照:
❌ 订单创建成功,但促销活动已失效
❌ 用户实际应付300元,但显示250元
❌ 资损50元/单

如果创单实时校验:
✅ 创单时重新查询营销活动
✅ 发现活动已失效
✅ 提示用户"活动已结束,当前价格为300元"
✅ 用户重新决策,无资损

关键设计原则(防御性设计):

“试算用快照(性能优先),创单强制校验(准确性优先)”

即使试算阶段使用了过期快照,最终创单时的实时校验会拦截所有不一致情况,用户最终支付的价格一定是准确的。

追问方向

  1. 如果用户在详情页看到价格X,试算也显示X,但创单时价格变成了Y,用户体验如何?

    • 这是正确行为(保证准确性)
    • 友好提示:”活动已结束,当前价格为Y,是否继续?”
    • 允许用户取消订单,无损失
    • 监控价格变化率,如果频繁变化说明快照TTL过长
  2. 快照命中率如何监控?目标是多少?

    • 监控指标:快照命中次数 / 总试算次数
    • 目标:> 80%(即80%用户在5分钟内从详情页到试算)
    • 如果命中率过低,说明用户决策时间长,可能需要优化用户体验
  3. 创单时如果营销校验失败,库存已预占怎么办?

    • Saga补偿机制:营销校验失败 → 立即释放库存
    • 事务顺序:先校验营销,再预占库存(避免无效预占)
    • 实际实现:营销校验 → 库存预占 → 计价 → 创单

答题要点

  • 试算vs创单差异(性能vs准确性)
  • 快照机制(5分钟有效期)
  • 创单强制实时校验(防御性设计)
  • 性能优化(缓存、防抖、并发)

加分项

  • 提及防御性设计理念
  • 提及快照命中率监控(80%)
  • 提及用户体验优化
  • 提及Saga补偿机制

Q10:如何处理”买2件享9折”这种多买优惠的计算?

考察点:复杂促销逻辑实现、算法设计、边界条件

参考答案

“买2件享9折”是典型的多买优惠(Multi-buy Promotion),需要判断用户购买数量是否满足条件。

促销类型分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type MultiBuyPromotion struct {
ID string
Type MultiBuyType
Conditions *MultiBuyCondition
Discount *DiscountRule
}

type MultiBuyType int
const (
MultiBuyTypeQuantity MultiBuyType = 1 // 买N件享折扣(买2件9折)
MultiBuyTypeTiered MultiBuyType = 2 // 阶梯折扣(买2件9折,买3件8折)
MultiBuyTypeBuyNGetM MultiBuyType = 3 // 买N送M(买2送1)
MultiBuyTypeBundled MultiBuyType = 4 // 组合购买(A+B一起买减X元)
)

type MultiBuyCondition struct {
MinQuantity int // 最少购买数量
ApplicableSkus []int64 // 适用的SKU列表(空=全场)
}

type DiscountRule struct {
Type DiscountType // rate(折扣率)/ reduce(减免金额)
Value float64 // 0.9(9折)/ 50.00(减50元)
}

场景1:买N件享折扣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用户购买:SKU 1001 × 2件(单价299元)
// 促销:买2件享9折

func (s *PricingService) applyMultiBuyPromotion(item *Item, promo *MultiBuyPromotion) float64 {
// 判断是否满足条件
if item.Quantity < promo.Conditions.MinQuantity {
return 0.0 // 不满足条件,无优惠
}

// 计算原价
originalPrice := item.UnitPrice * float64(item.Quantity)
// 299 * 2 = 598

// 应用折扣
discountedPrice := originalPrice * promo.Discount.Value
// 598 * 0.9 = 538.2

// 优惠金额
discount := originalPrice - discountedPrice
// 598 - 538.2 = 59.8

return discount
}

场景2:阶梯折扣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 用户购买:SKU 1001 × 3件
// 促销:买2件9折,买3件8折(阶梯式)

type TieredDiscount struct {
Tiers []Tier
}

type Tier struct {
MinQuantity int
Discount float64
}

func (s *PricingService) applyTieredPromotion(item *Item, promo *TieredPromotion) float64 {
// 找到适用的档位(从高到低查找)
var applicableTier *Tier
for i := len(promo.Tiers) - 1; i >= 0; i-- {
if item.Quantity >= promo.Tiers[i].MinQuantity {
applicableTier = &promo.Tiers[i]
break
}
}

if applicableTier == nil {
return 0.0 // 不满足任何档位
}

// 应用折扣
originalPrice := item.UnitPrice * float64(item.Quantity)
discountedPrice := originalPrice * applicableTier.Discount

return originalPrice - discountedPrice
}

// 示例
item := &Item{SkuID: 1001, UnitPrice: 299, Quantity: 3}
promo := &TieredPromotion{
Tiers: []Tier{
{MinQuantity: 2, Discount: 0.9}, // 买2件9折
{MinQuantity: 3, Discount: 0.8}, // 买3件8折
},
}
discount := applyTieredPromotion(item, promo)
// 299*3 = 897, 897*0.8 = 717.6, 优惠179.4元

场景3:买N送M

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 用户购买:SKU 1001 × 3件
// 促销:买2送1(即买2件的价格买3件)

func (s *PricingService) applyBuyNGetMPromotion(item *Item, promo *BuyNGetMPromotion) float64 {
buyN := promo.BuyQuantity // 2
getM := promo.GetQuantity // 1

// 计算可以享受的套数
sets := item.Quantity / (buyN + getM)
// 3 / (2+1) = 1套

// 计算赠送数量
freeQuantity := sets * getM
// 1 * 1 = 1件

// 优惠金额
discount := item.UnitPrice * float64(freeQuantity)
// 299 * 1 = 299

return discount
}

场景4:组合购买(跨SKU)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用户购买:耳机(SKU 1001)+ 充电器(SKU 1005)
// 促销:搭配购买立减50元

type BundlePromotion struct {
ID string
BundleSkus []int64 // [1001, 1005]
DiscountType string // reduce
DiscountAmount float64 // 50.00
}

func (s *PricingService) applyBundlePromotion(items []*Item, promo *BundlePromotion) float64 {
// 判断用户是否购买了所有必需的SKU
userSkuIDs := extractSkuIDs(items)

for _, requiredSkuID := range promo.BundleSkus {
if !contains(userSkuIDs, requiredSkuID) {
return 0.0 // 未购买全部商品,不满足条件
}
}

// 满足条件,返回优惠金额
return promo.DiscountAmount // 50.00
}

复杂场景:多个多买优惠叠加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 用户购买:
// - SKU 1001(耳机) × 2件,单价299元
// - SKU 1005(充电器) × 1件,单价89元
// - SKU 2003(数据线) × 3件,单价19元

// 促销活动:
// - P1: 耳机买2件9折(商品级)
// - P2: 配件类买3件8折(品类级)
// - P3: 满300减50(订单级)

func (s *PricingService) CalculateFinalPrice(items []*Item, promos []*Promotion) *PriceDetail {
// 1. 商品原价
subtotal := 299*2 + 89*1 + 19*3 = 598 + 89 + 57 = 744

// 2. 应用商品级促销(P1)
// 耳机:598 * 0.9 = 538.2,优惠59.8
itemDiscount := 59.8

// 3. 应用品类级促销(P2)
// 充电器(89) + 数据线(57) = 146,满足"配件类买3件"
// 但品类折扣与商品折扣互斥吗?
// 这里需要明确规则:

// 策略A:品类折扣只应用于未享受商品折扣的商品
categoryApplicableItems := filterOutDiscounted(items, itemDiscounted)
// 充电器+数据线:146 * 0.8 = 116.8,优惠29.2

// 策略B:品类折扣应用于所有品类商品(会更优惠)
categoryPrice := (89 + 57) * 0.8 = 116.8,优惠29.2

// 4. 应用订单级促销(P3)
afterItemAndCategory := 744 - 59.8 - 29.2 = 655
if 655 >= 300 {
orderDiscount = 50
}

// 最终:744 - 59.8 - 29.2 - 50 = 605元
}

关键难点:促销互斥规则

规则类型 说明 示例
完全互斥 只能享受一个优惠 9折和8折互斥,取最优
跨级叠加 不同层级可叠加 商品9折 + 订单满减可叠加
部分叠加 同级部分商品可叠加 耳机9折 + 配件8折(不同商品)
互斥组 同组互斥 “会员折扣组”内互斥

追问方向

  1. 如果用户买了2件耳机,同时满足”买2件9折”和”满300减50”,如何计算?

    • 按层级依次计算:商品级9折 → 订单级满减
    • 598*0.9 = 538.2,满足300 → 538.2 - 50 = 488.2
  2. 如何支持”买2件9折,买3件8折,买5件7折”这种阶梯折扣?

    • 使用TieredPromotion模型
    • 从高到低查找适用档位
    • 返回最优折扣
  3. 如何测试多买优惠的正确性?

    • 单元测试:覆盖所有边界条件(买1件、2件、3件)
    • 参数化测试:不同数量组合的测试用例
    • 比对测试:与老系统空跑比对
  4. 如何防止促销配置错误(如”买1件9折”变成”买0件9折”)?

    • 配置校验:MinQuantity >= 1
    • 促销审核流程:运营配置 → 审核 → 上线
    • 沙箱测试:上线前在测试环境验证

答题要点

  • 多买优惠类型(数量、阶梯、买N送M、组合)
  • 促销互斥规则
  • 跨SKU促销计算
  • 边界条件处理

加分项

  • 提及配置化规则引擎
  • 提及测试策略(单元测试、参数化测试)
  • 提及审核流程
  • 提及防御性校验

Q11:计价服务应该自己调用Marketing Service,还是由聚合服务传入营销数据?(ADR-001)

考察点:服务边界设计、依赖解耦、架构决策能力

参考答案

这是一个经典的架构决策问题(文档ADR-001)。我们最终选择由聚合服务获取营销数据后传递给计价服务

两种方案对比

方案1:Pricing Service自己调用Marketing Service

1
Aggregation → Pricing → Marketing(3层调用链)
1
2
3
4
5
6
7
8
9
10
11
func (s *PricingService) CalculatePrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
// Pricing自己调用Marketing获取促销信息
promos, err := s.marketingClient.GetPromotions(ctx, req.SkuIDs, req.UserID)
if err != nil {
return nil, err
}

// 计算价格
finalPrice := s.calculate(req.BasePrice, promos)
return &PriceResponse{FinalPrice: finalPrice}, nil
}

方案2:Aggregation Service传入营销数据(✅ 采纳)

1
Aggregation → Pricing | Marketing(2层调用链,并行)
1
2
3
4
5
6
7
8
9
// Aggregation获取营销数据
promos := aggregationService.marketingClient.GetPromotions(ctx, skuIDs, userID)

// 传给Pricing Service
prices := aggregationService.pricingClient.CalculatePrice(ctx, &PriceRequest{
SkuIDs: skuIDs,
BasePrices: basePrices,
Promos: promos, // 传入营销数据
})

采纳方案2的5个理由

理由1:单一职责原则(SRP)

  • Pricing Service应该是纯计算服务,只负责价格计算逻辑
  • 不应该关心数据从哪里来(Product、Marketing、Inventory)
  • 输入参数标准化:base_price + promo_info → final_price
1
2
3
4
5
6
7
8
9
10
11
// ✅ 好的设计:Pricing是纯函数
func (s *PricingService) Calculate(basePrice float64, promo *PromoInfo) float64 {
return basePrice * promo.DiscountRate // 纯计算,无IO
}

// ❌ 不好的设计:Pricing有IO操作
func (s *PricingService) Calculate(skuID int64) float64 {
basePrice := s.productClient.GetPrice(skuID) // IO依赖
promo := s.marketingClient.GetPromo(skuID) // IO依赖
return basePrice * promo.DiscountRate
}

理由2:依赖解耦

1
2
3
4
5
方案1依赖链:Aggregation → Pricing → Marketing(传递性依赖)
问题:Aggregation依赖Marketing(间接),耦合度高

方案2依赖链:Aggregation → Pricing | Marketing(平行依赖)✓
优点:Aggregation显式依赖Marketing,依赖关系清晰

理由3:性能优化空间更大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 方案2:Aggregation可以并发调用多个服务
var wg sync.WaitGroup
wg.Add(3)

go func() {
products = productClient.BatchGet(skuIDs) // 并发1
wg.Done()
}()

go func() {
stocks = inventoryClient.BatchCheck(skuIDs) // 并发2
wg.Done()
}()

go func() {
promos = marketingClient.GetPromotions(skuIDs) // 并发3
wg.Done()
}()

wg.Wait()

// 然后调用Pricing(无IO,纯计算,快)
prices = pricingClient.Calculate(products, promos)

// 总耗时:max(50ms, 30ms, 70ms) + 20ms = 90ms
1
2
3
4
5
6
// 方案1:Pricing串行调用Marketing
products = productClient.BatchGet(skuIDs) // 50ms
stocks = inventoryClient.BatchCheck(skuIDs) // 30ms
prices = pricingClient.Calculate(skuIDs) // 内部调用Marketing:70ms + 计算20ms = 90ms

// 总耗时:50 + 30 + 90 = 170ms(更慢)

理由4:易于测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 方案2:Pricing是纯函数,测试简单
func TestCalculatePrice(t *testing.T) {
priceItem := &PriceCalculateItem{
SkuID: 1001,
BasePrice: 2399.00,
PromoInfo: &PromoInfo{DiscountRate: 0.9}, // Mock数据,无需真实Marketing
}

result := pricingService.Calculate(priceItem)

assert.Equal(t, 2159.10, result.FinalPrice)
assert.Equal(t, 239.90, result.Discount)
}

// 方案1:Pricing有IO,测试复杂
func TestCalculatePrice(t *testing.T) {
// 需要Mock Marketing Client
mockMarketing := &MockMarketingClient{...}
pricingService := NewPricingService(mockMarketing)

result := pricingService.Calculate(1001) // 内部会调用mockMarketing
// ...
}

理由5:统一降级处理

  • 聚合层统一处理各服务失败(Marketing、Product、Inventory)
  • Pricing Service无感知,始终收到完整输入数据
  • 降级逻辑不混入业务计算
1
2
3
4
5
6
7
8
9
// Aggregation层统一降级
promos, err := aggregation.marketingClient.GetPromotions(ctx, skuIDs)
if err != nil {
// 降级:Marketing失败,使用空促销
promos = make(map[int64]*PromoInfo)
}

// Pricing收到的数据是完整的(要么真实促销,要么空促销)
prices := aggregation.pricingClient.Calculate(ctx, basePrices, promos)

方案2的架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌────────────────────────────────────────┐
│ Aggregation Service(编排层) │
│ │
│ ┌──────────────────────────────────┐ │
│ │ SearchOrchestrator │ │
│ │ 1. ES查询 → sku_ids │ │
│ │ 2. 并发调用: │ │
│ │ ├─ Product (base_price) │ │
│ │ ├─ Inventory (stock) │ │
│ │ └─ Marketing (promo_info) ✓ │ │
│ │ 3. 串行调用: │ │
│ │ └─ Pricing (传入promo_info)✓ │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
↓ ↓
┌──────────────┐ ┌──────────────┐
│ Marketing │ │ Pricing │
│ Service │ │ Service │
│ (数据源) │ │ (纯计算)✓ │
└──────────────┘ └──────────────┘

追问方向

  1. 如果Pricing内部需要根据营销类型做不同计算怎么办?

    • Aggregation传入的PromoInfo已包含类型信息
    • Pricing根据类型分发到不同Calculator
    • 例如:折扣型Calculator、满减型Calculator
  2. 方案2会导致Aggregation层逻辑过重吗?

    • 这是Aggregation的职责(数据编排)
    • 遵循”胖编排、瘦服务”原则
    • Aggregation复杂度增加,但整体系统更解耦
  3. 如果有多个场景都需要调用Pricing,都要先获取Marketing吗?

    • 是的,这是设计一致性
    • 但可以复用Aggregation的编排逻辑(代码复用)
    • 不同场景可以有不同的编排器(SearchOrchestrator、DetailOrchestrator)

答题要点

  • 单一职责原则(SRP)
  • 依赖解耦(显式依赖)
  • 性能优化(并发调用)
  • 易于测试(纯函数)
  • 统一降级

加分项

  • 提及架构决策记录(ADR)
  • 提及”胖编排、瘦服务”原则
  • 提及纯函数设计理念
  • 对比两种方案的性能数据

常见误区

  • ❌ 认为方案1更简单(忽略了耦合度问题)
  • ❌ 认为方案2增加了网络调用次数(实际是并行的)
  • ❌ 无法说明具体的设计原则

Q12:如何设计价格的可追溯性?用户投诉价格不对如何快速定位?

考察点:可观测性设计、问题定位能力、日志设计

参考答案

价格计算涉及资金,用户投诉”价格不对”时必须快速定位问题。我们设计了价格明细对象(PriceBreakdown)+ 全链路追踪

核心设计:PriceBreakdown值对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
type PriceBreakdown struct {
OrderID string `json:"order_id"`
SkuID int64 `json:"sku_id"`
Timestamp int64 `json:"timestamp"`

// 四层价格明细
BasePrice float64 `json:"base_price"` // 基础价格

PromotionDetails []*PromotionDetail `json:"promotion_details"` // 营销详情
TotalPromotion float64 `json:"total_promotion"` // 营销总优惠

FeeDetails []*FeeDetail `json:"fee_details"` // 费用详情
TotalFee float64 `json:"total_fee"` // 总费用

VoucherDetails []*VoucherDetail `json:"voucher_details"` // 优惠券详情
TotalVoucher float64 `json:"total_voucher"` // 优惠券总额

// 最终价格
FinalPrice float64 `json:"final_price"`

// 计算过程追踪
CalculationSteps []string `json:"calculation_steps"`

// 快照信息
SnapshotID string `json:"snapshot_id,omitempty"`
SnapshotUsed bool `json:"snapshot_used"`
}

type PromotionDetail struct {
PromoID string `json:"promo_id"`
PromoName string `json:"promo_name"`
PromoType string `json:"promo_type"` // discount/reduce/bundle
Level string `json:"level"` // item/category/order
DiscountAmount float64 `json:"discount_amount"`
Applied bool `json:"applied"` // 是否生效
Reason string `json:"reason,omitempty"` // 未生效原因
}

type FeeDetail struct {
FeeType string `json:"fee_type"` // service_fee/tax/channel_fee
Amount float64 `json:"amount"`
Rate float64 `json:"rate"`
}

type VoucherDetail struct {
VoucherID string `json:"voucher_id"`
VoucherCode string `json:"voucher_code"`
DiscountType string `json:"discount_type"` // reduce/rate
DiscountAmount float64 `json:"discount_amount"`
Applied bool `json:"applied"`
}

计算过程追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
func (s *PricingService) CalculateWithBreakdown(items []*Item, promos []*Promotion) *PriceBreakdown {
breakdown := &PriceBreakdown{
OrderID: generateOrderID(),
Timestamp: time.Now().Unix(),
CalculationSteps: make([]string, 0),
}

// Step 1: 计算商品原价
subtotal := calculateSubtotal(items) // 744.00
breakdown.BasePrice = subtotal
breakdown.addStep(fmt.Sprintf("商品原价: %.2f元", subtotal))

// Step 2: 应用商品级促销
itemDiscounts := make([]*PromotionDetail, 0)
totalItemDiscount := 0.0
for _, item := range items {
if promo := findItemPromo(item.SkuID, promos); promo != nil {
discount := item.Price * float64(item.Quantity) * (1 - promo.DiscountRate)
totalItemDiscount += discount

itemDiscounts = append(itemDiscounts, &PromotionDetail{
PromoID: promo.ID,
PromoName: promo.Name,
PromoType: "discount",
Level: "item",
DiscountAmount: discount,
Applied: true,
})

breakdown.addStep(fmt.Sprintf("商品%d应用促销%s: -%.2f元",
item.SkuID, promo.Name, discount))
}
}

// Step 3: 应用订单级促销
afterItem := subtotal - totalItemDiscount
orderDiscount := 0.0
for _, promo := range findOrderPromos(promos) {
if afterItem >= promo.Threshold {
orderDiscount += promo.ReduceAmount
breakdown.addStep(fmt.Sprintf("订单满减%s: -%.2f元", promo.Name, promo.ReduceAmount))
} else {
// 记录未生效的促销(重要:用户可能疑惑"为什么没有优惠")
itemDiscounts = append(itemDiscounts, &PromotionDetail{
PromoID: promo.ID,
PromoName: promo.Name,
PromoType: "reduce",
Level: "order",
DiscountAmount: 0,
Applied: false,
Reason: fmt.Sprintf("订单金额%.2f元未达到%.2f元门槛", afterItem, promo.Threshold),
})
}
}

// Step 4: 应用优惠券
// ...类似逻辑

// Step 5: 最终价格
finalPrice := subtotal - totalItemDiscount - orderDiscount - couponDiscount
breakdown.FinalPrice = finalPrice
breakdown.addStep(fmt.Sprintf("最终价格: %.2f元", finalPrice))

breakdown.PromotionDetails = itemDiscounts
breakdown.TotalPromotion = totalItemDiscount + orderDiscount

return breakdown
}

// 添加计算步骤
func (b *PriceBreakdown) addStep(step string) {
b.CalculationSteps = append(b.CalculationSteps, step)
}

存储策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建订单时存储PriceBreakdown
func (s *OrderService) CreateOrder(ctx context.Context, order *Order, breakdown *PriceBreakdown) error {
// Step 1: 存储订单主表
if err := s.orderRepo.Create(ctx, order); err != nil {
return err
}

// Step 2: 存储价格明细(JSON)
breakdownJSON, _ := json.Marshal(breakdown)
if err := s.orderRepo.SavePriceBreakdown(ctx, order.OrderID, breakdownJSON); err != nil {
log.Error("save price breakdown failed", "order_id", order.OrderID, "error", err)
// 不影响主流程,只记录错误
}

// Step 3: 异步写入ES(用于快速检索)
go func() {
s.esClient.IndexPriceBreakdown(context.Background(), breakdown)
}()

return nil
}

数据库设计

1
2
3
4
5
6
7
8
9
CREATE TABLE order_price_breakdown (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
breakdown_json JSON NOT NULL COMMENT '价格明细JSON',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

INDEX idx_order_id (order_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB COMMENT='订单价格明细表';

问题定位流程

场景:用户投诉”我看到的价格是250元,为什么实际扣款300元?”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Step 1: 查询订单价格明细
breakdown, _ := orderService.GetPriceBreakdown(orderID)

// Step 2: 分析明细
fmt.Println("价格计算过程:")
for _, step := range breakdown.CalculationSteps {
fmt.Println(step)
}
// 输出:
// 商品原价: 300.00元
// 商品1001应用促销"限时折扣": -50.00元
// 订单满减"满300减50": 未生效(订单金额250.00元未达到300.00元门槛)
// 最终价格: 250.00元

// Step 3: 对比用户看到的价格
// 发现:用户看到的250元是试算阶段的快照数据
// 快照中促销"限时折扣"有效
// 但创单时促销已失效(库存用尽)

// Step 4: 查询促销活动历史
promo, _ := marketingService.GetPromotionHistory(promoID, timestamp)
// 发现:促销在试算时有效,创单时已失效

// Step 5: 给出结论
// 用户在详情页看到价格(快照)时促销有效
// 5分钟后创单时促销库存已用尽
// 系统行为正确:创单时实时校验,拒绝使用失效促销
// 用户看到的250元是快照价格,实际价格是300元

全链路追踪(Distributed Tracing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 使用OpenTracing/Jaeger追踪价格计算链路
func (s *PricingService) Calculate(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
span, ctx := opentracing.StartSpanFromContext(ctx, "PricingService.Calculate")
defer span.Finish()

// 记录输入参数
span.SetTag("order_id", req.OrderID)
span.SetTag("sku_ids", req.SkuIDs)
span.SetTag("user_id", req.UserID)

// Step 1: 计算基础价格
subtotal := calculateSubtotal(req.Items)
span.LogKV("event", "base_price_calculated", "amount", subtotal)

// Step 2: 应用促销
discount := applyPromotions(req.Items, req.Promos)
span.LogKV("event", "promotion_applied", "discount", discount)

// Step 3: 最终价格
finalPrice := subtotal - discount
span.SetTag("final_price", finalPrice)

return &PriceResponse{FinalPrice: finalPrice}, nil
}

通过Jaeger UI可以看到完整的调用链路和每个步骤的耗时。

追问方向

  1. PriceBreakdown存储在哪里?为什么?

    • MySQL:订单表关联表,JSON字段存储
    • ES:快速检索和分析(按促销ID、金额范围查询)
    • Redis:不存储(太大,不常访问)
  2. 如何快速查询”哪些订单使用了促销P001”?

    • ES索引:按promo_id查询
    • SQL查询:SELECT * FROM order_price_breakdown WHERE JSON_CONTAINS(breakdown_json, '{"promo_id":"P001"}')
  3. 如果用户投诉价格不对,平均定位时间是多少?

    • 目标:< 5分钟
    • 通过PriceBreakdown + Jaeger链路追踪
    • 对比快照数据与实际数据
  4. PriceBreakdown会占用多大存储空间?

    • 单条约2KB(JSON)
    • 日订单200万 → 4GB/天
    • 保留90天 → 360GB
    • 使用JSON压缩可减少50%

答题要点

  • PriceBreakdown值对象设计
  • 计算步骤追踪
  • 全链路追踪(Jaeger)
  • ES索引快速检索

加分项

  • 提及DDD值对象设计
  • 提及可观测性体系
  • 提及具体定位时间(5分钟)
  • 提及存储成本估算

Q13:如何防止营销活动配置错误导致资损?

考察点:风险控制、防御性设计、流程设计

参考答案

营销活动配置错误是电商系统的高发风险,必须建立多层防护机制。

典型资损场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
场景1:折扣配置错误
- 运营想配置"9折"(0.9)
- 误配置为"0.09"(0.09折,即原价的9%)
- 后果:用户299元商品只付26.91元
- 资损:272.09元/单

场景2:满减门槛错误
- 运营想配置"满1000减50"
- 误配置为"满10减50"
- 后果:用户买11元商品可用50元优惠券
- 资损:39元/单,可能被薅羊毛

场景3:叠加规则错误
- 运营想配置"单品折扣与满减互斥"
- 误配置为"可叠加"
- 后果:用户享受双重优惠
- 资损:额外优惠金额

场景4:时间配置错误
- 运营想配置"2026-11-11 00:00:00生效"
- 误配置为"2025-11-11 00:00:00"(去年)
- 后果:促销提前一年生效
- 资损:巨大(取决于发现时间)

五层防护机制

第一层:配置校验(前端 + 后端双重校验)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
type PromotionValidator struct {}

func (v *PromotionValidator) Validate(promo *Promotion) error {
errors := make([]string, 0)

// 1. 折扣率范围校验
if promo.DiscountRate != nil {
if *promo.DiscountRate < 0.1 || *promo.DiscountRate > 1.0 {
errors = append(errors,
fmt.Sprintf("折扣率%.2f不合法,必须在0.1-1.0之间", *promo.DiscountRate))
}
}

// 2. 满减门槛校验
if promo.ReduceAmount != nil && promo.Threshold != nil {
if *promo.ReduceAmount >= *promo.Threshold {
errors = append(errors,
fmt.Sprintf("优惠金额%.2f不能大于等于门槛%.2f", *promo.ReduceAmount, *promo.Threshold))
}

// 合理性校验:优惠金额通常不超过门槛的50%
if *promo.ReduceAmount > *promo.Threshold * 0.5 {
errors = append(errors,
fmt.Sprintf("警告:优惠金额%.2f过大(超过门槛的50%%),请确认", *promo.ReduceAmount))
}
}

// 3. 时间范围校验
if promo.StartTime.After(promo.EndTime) {
errors = append(errors, "开始时间不能晚于结束时间")
}

if promo.StartTime.Before(time.Now().Add(-365 * 24 * time.Hour)) {
errors = append(errors, "开始时间不能早于1年前(可能是配置错误)")
}

// 4. 库存限量校验
if promo.StockLimit != nil && *promo.StockLimit <= 0 {
errors = append(errors, "库存限量必须>0")
}

// 5. 互斥规则校验
if len(promo.ExcludeWith) > 0 {
for _, excludePromoID := range promo.ExcludeWith {
if !s.promoExists(excludePromoID) {
errors = append(errors,
fmt.Sprintf("互斥促销%s不存在", excludePromoID))
}
}
}

if len(errors) > 0 {
return fmt.Errorf("促销配置校验失败:\n%s", strings.Join(errors, "\n"))
}

return nil
}

第二层:审批流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 促销创建需要审批
func (s *MarketingService) CreatePromotion(ctx context.Context, promo *Promotion, operator string) error {
// 1. 配置校验
if err := s.validator.Validate(promo); err != nil {
return err
}

// 2. 创建审批工单
approvalTask := &ApprovalTask{
TaskID: generateTaskID(),
Type: "promotion_create",
Content: promo,
Operator: operator, // 配置人
Status: "PENDING",
CreatedAt: time.Now(),
}

// 3. 根据优惠金额分配审批人
if promo.EstimatedBudget < 10000 {
approvalTask.Approver = "marketing_lead" // 1万以下:营销组长审批
} else if promo.EstimatedBudget < 100000 {
approvalTask.Approver = "marketing_director" // 10万以下:营销总监审批
} else {
approvalTask.Approver = "cfo" // 10万以上:CFO审批
}

if err := s.approvalService.CreateTask(ctx, approvalTask); err != nil {
return err
}

// 4. 促销状态设为"待审批"
promo.Status = PromotionStatusPendingApproval
return s.repo.Create(ctx, promo)
}

第三层:沙箱测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 上线前在沙箱环境模拟测试
func (s *MarketingService) SimulatePromotion(ctx context.Context, promoID string, testCases []*TestCase) (*SimulationReport, error) {
promo, _ := s.repo.GetByID(ctx, promoID)

report := &SimulationReport{
PromoID: promoID,
TestCases: make([]*TestResult, 0),
}

// 执行测试用例
for _, tc := range testCases {
result := s.calculatePrice(tc.Items, []*Promotion{promo})

// 比对预期结果
testResult := &TestResult{
CaseName: tc.Name,
Input: tc.Items,
ExpectedPrice: tc.ExpectedPrice,
ActualPrice: result.FinalPrice,
Passed: math.Abs(tc.ExpectedPrice - result.FinalPrice) < 0.01,
}

report.TestCases = append(report.TestCases, testResult)
}

// 生成报告
report.PassRate = calculatePassRate(report.TestCases)

return report, nil
}

// 测试用例示例
testCases := []*TestCase{
{
Name: "买1件不享受优惠",
Items: []*Item{{SkuID: 1001, Price: 299, Quantity: 1}},
ExpectedPrice: 299.00,
},
{
Name: "买2件享9折",
Items: []*Item{{SkuID: 1001, Price: 299, Quantity: 2}},
ExpectedPrice: 538.20, // 299*2*0.9
},
{
Name: "买3件享9折(边界条件)",
Items: []*Item{{SkuID: 1001, Price: 299, Quantity: 3}},
ExpectedPrice: 807.30, // 299*3*0.9
},
}

第四层:灰度发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 促销活动灰度发布(1%→10%→50%→100%)
func (s *MarketingService) shouldApplyPromotion(userID int64, promo *Promotion) bool {
// 如果促销在灰度中
if promo.GrayPercentage < 100 {
hash := fnv1a(userID)
bucket := hash % 100
return bucket < promo.GrayPercentage
}

return true // 全量发布
}

// 灰度监控:观察促销效果
type PromotionGrayMetrics struct {
PromoID string
GrayPercentage int
AppliedCount int64 // 生效次数
DiscountAmount float64 // 总优惠金额
OrderConversion float64 // 订单转化率
AvgOrderValue float64 // 平均客单价
}

第五层:实时监控告警

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 异常价格告警
type PriceAnomalyDetector struct {
alerting AlertService
}

func (d *PriceAnomalyDetector) CheckAnomaly(breakdown *PriceBreakdown) {
// 1. 负价格告警(P0)
if breakdown.FinalPrice < 0 {
d.alerting.SendUrgentAlert(
"检测到负价格",
fmt.Sprintf("订单%s最终价格%.2f元<0", breakdown.OrderID, breakdown.FinalPrice),
"@pricing-team @sre-oncall")
}

// 2. 零价格告警(P1)
if breakdown.FinalPrice == 0 {
d.alerting.SendAlert(
"检测到零价格",
fmt.Sprintf("订单%s最终价格为0", breakdown.OrderID))
}

// 3. 极端折扣告警(P2)
discountRate := breakdown.TotalPromotion / breakdown.BasePrice
if discountRate > 0.9 {
d.alerting.SendAlert(
"检测到极端折扣",
fmt.Sprintf("订单%s折扣率%.2f%%>90%%", breakdown.OrderID, discountRate*100))
}

// 4. 异常优惠金额告警
if breakdown.TotalPromotion > breakdown.BasePrice * 0.8 {
d.alerting.SendAlert(
"优惠金额异常",
fmt.Sprintf("订单%s优惠%.2f元,接近原价%.2f元",
breakdown.OrderID, breakdown.TotalPromotion, breakdown.BasePrice))
}
}

监控大盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
营销活动监控大盘:

实时指标:
• 促销生效订单数:12,500单/小时
• 总优惠金额:¥125万/小时
• 平均优惠金额:¥100/单
• 预算消耗进度:35%(预算¥1000万,已用¥350万)

异常告警:
• 极端折扣订单:0单(P2告警)
• 负价格订单:0单(P0告警)
• 零价格订单:0单(P1告警)
• 预算超支:否

促销效果:
• 订单转化率:+15%(对比无促销)
• 客单价:+25%(多买优惠生效)
• 用户投诉:2单(0.016%)

追问方向

  1. 如果已经发生资损(配置错误已上线),如何快速止损?

    • 紧急下线促销(状态设为INACTIVE)
    • 清理所有缓存(Redis、本地缓存)
    • 已生成的订单人工审核(如果金额异常,主动联系用户)
    • 估算损失金额,提交事故报告
  2. 审批流程会不会降低运营效率?

    • 低风险促销(折扣率>0.8,金额<1万)可自动审批
    • 高风险促销必须人工审批
    • 紧急促销有快速通道(15分钟内审批)
  3. 如何防止薅羊毛(用户恶意利用促销规则)?

    • 用户参与次数限制(如每人最多参与5次)
    • 异常行为检测(如短时间内大量下单)
    • 风控系统联动(高风险用户限制)
  4. 沙箱测试的测试用例如何设计?

    • 正常场景:满足条件、不满足条件
    • 边界条件:恰好满足、差一点点
    • 极端场景:购买数量极大、金额极小
    • 组合场景:与其他促销叠加

答题要点

  • 五层防护(校验、审批、沙箱、灰度、监控)
  • 配置校验规则
  • 审批流程分级
  • 实时监控告警

加分项

  • 提及具体资损案例
  • 提及防薅羊毛措施
  • 提及监控指标(极端折扣、负价格)
  • 提及紧急止损流程

1.4 价格计算性能优化

Q14:如何优化批量价格计算的性能?

考察点:批量优化、并发编程、性能调优

参考答案

批量价格计算是高频场景(搜索列表、购物车、批量导入),性能优化至关重要。

优化前的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ❌ 坏的实践:循环调用单个接口
func (s *AggregationService) Search(ctx context.Context, skuIDs []int64) ([]*Item, error) {
items := make([]*Item, 0, len(skuIDs))

for _, skuID := range skuIDs {
// 问题1:N次RPC调用(20个商品 = 20次RPC)
product := s.productClient.GetProduct(ctx, skuID) // 50ms each

// 问题2:N次RPC调用
promo := s.marketingClient.GetPromotion(ctx, skuID, userID) // 30ms each

// 问题3:N次RPC调用
price := s.pricingClient.Calculate(ctx, product, promo) // 20ms each

items = append(items, &Item{Product: product, Price: price})
}

// 总耗时:20 * (50 + 30 + 20) = 2000ms(不可接受)
return items, nil
}

优化策略

优化1:批量接口(Batch API)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 好的实践:批量接口
func (s *AggregationService) Search(ctx context.Context, skuIDs []int64) ([]*Item, error) {
// 1次RPC批量查询20个商品
products := s.productClient.BatchGetProducts(ctx, skuIDs) // 50ms

// 1次RPC批量查询20个促销
promos := s.marketingClient.BatchGetPromotions(ctx, skuIDs, userID) // 30ms

// 1次RPC批量计算20个价格
prices := s.pricingClient.BatchCalculatePrice(ctx, products, promos) // 20ms

// 总耗时:50 + 30 + 20 = 100ms(提升20倍)✓
return buildItems(products, promos, prices), nil
}

批量接口设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Product Service批量接口
func (s *ProductService) BatchGetProducts(ctx context.Context, skuIDs []int64) ([]*Product, error) {
// 1. 参数校验(防止超大批量)
if len(skuIDs) > 100 {
return nil, fmt.Errorf("批量查询不能超过100个SKU")
}

// 2. 去重
uniqueIDs := unique(skuIDs)

// 3. 批量查询MySQL(使用IN子句)
query := "SELECT * FROM product WHERE sku_id IN (?)"
products, err := s.db.Query(query, uniqueIDs)
if err != nil {
return nil, err
}

return products, nil
}

优化2:并发调用(Concurrent Calls)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 并发调用无依赖的服务
func (s *AggregationService) Search(ctx context.Context, skuIDs []int64) ([]*Item, error) {
var wg sync.WaitGroup
var products []*Product
var stocks map[int64]*StockInfo
var errChan = make(chan error, 2)

wg.Add(2)

// 并发调用1:Product
go func() {
defer wg.Done()
var err error
products, err = s.productClient.BatchGetProducts(ctx, skuIDs)
if err != nil {
errChan <- err
}
}()

// 并发调用2:Inventory
go func() {
defer wg.Done()
var err error
stocks, err = s.inventoryClient.BatchCheckStock(ctx, skuIDs)
if err != nil {
errChan <- err
}
}()

wg.Wait()
close(errChan)

// 检查错误
for err := range errChan {
if err != nil {
return nil, err
}
}

// 串行调用有依赖的服务
promos := s.marketingClient.GetPromotions(ctx, skuIDs) // 需要skuIDs
prices := s.pricingClient.Calculate(ctx, products, promos) // 需要products + promos

// 总耗时:max(50ms, 30ms) + 70ms + 20ms = 140ms
// vs 串行:50 + 30 + 70 + 20 = 170ms
return buildItems(products, stocks, promos, prices), nil
}

优化3:分批处理(Chunking)

当SKU数量很大时(如100+),分批处理避免超时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (s *PricingService) BatchCalculatePrice(ctx context.Context, items []*PriceItem) (map[int64]*Price, error) {
const chunkSize = 50 // 每批50个

results := make(map[int64]*Price)

// 分批处理
for i := 0; i < len(items); i += chunkSize {
end := min(i+chunkSize, len(items))
chunk := items[i:end]

// 处理当前批次
chunkResults, err := s.calculateChunk(ctx, chunk)
if err != nil {
return nil, err
}

// 合并结果
for skuID, price := range chunkResults {
results[skuID] = price
}
}

return results, nil
}

优化4:缓存预热(Cache Prewarming)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 大促前预热热门商品价格
func (job *PricePrewarmJob) Run(ctx context.Context) {
// 1. 查询热门商品(销量Top 1000)
hotSkus := job.analyticsService.GetHotSkus(1000)

// 2. 批量计算价格
for i := 0; i < len(hotSkus); i += 50 {
chunk := hotSkus[i:min(i+50, len(hotSkus))]

// 查询商品和促销
products := job.productClient.BatchGetProducts(ctx, chunk)
promos := job.marketingClient.BatchGetPromotions(ctx, chunk, 0) // userID=0表示通用促销

// 计算价格
prices := job.pricingClient.BatchCalculatePrice(ctx, products, promos)

// 写入Redis(30分钟TTL)
for skuID, price := range prices {
cacheKey := fmt.Sprintf("price:sku:%d", skuID)
job.redis.Set(cacheKey, serialize(price), 30*time.Minute)
}
}

log.Info("Price prewarming completed", "count", len(hotSkus))
}

性能对比

优化措施 优化前 优化后 提升
循环RPC → 批量RPC 2000ms(20个商品) 100ms 20倍
串行调用 → 并发调用 170ms 140ms 1.2倍
分批处理 超时(1000个商品) 2秒 不超时
缓存预热 冷启动300ms 缓存命中5ms 60倍

追问方向

  1. 批量接口如何防止超大批量导致OOM?

    • 限制批量大小(最多100个)
    • 超过限制自动分批
    • 内存监控(Go pprof)
  2. 并发调用如何控制goroutine数量?

    • 使用Worker Pool模式
    • 限制最大并发数(如10个goroutine)
    • 使用semaphore控制
  3. 缓存预热的时机如何选择?

    • 大促前1天开始预热
    • 凌晨低峰期执行(减少对线上的影响)
    • 增量预热(只预热变化的商品)
  4. 如何监控批量接口的性能?

    • 监控批量大小分布(P50/P95/P99)
    • 监控批量接口延迟
    • 监控批量接口错误率

答题要点

  • 批量接口设计
  • 并发调用(goroutine)
  • 分批处理(chunking)
  • 缓存预热

加分项

  • 提及具体性能提升数据(20倍)
  • 提及Worker Pool模式
  • 提及内存优化(防OOM)
  • 提及监控指标

Q15:如何处理价格的国际化(多币种)?

考察点:国际化设计、汇率处理、数据一致性

参考答案

B2B2C电商需要支持多个国家/地区,涉及多币种价格展示和计算。

设计要点

1. 币种配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Currency struct {
Code string // "CNY", "USD", "SGD", "THB"
Symbol string // "¥", "$", "S$", "฿"
Precision int // 小数位数:CNY=2, JPY=0
Rate float64 // 对USD的汇率
}

var SupportedCurrencies = map[string]*Currency{
"CNY": {Code: "CNY", Symbol: "¥", Precision: 2, Rate: 7.2},
"USD": {Code: "USD", Symbol: "$", Precision: 2, Rate: 1.0}, // 基准币种
"SGD": {Code: "SGD", Symbol: "S$", Precision: 2, Rate: 1.35},
"THB": {Code: "THB", Symbol: "฿", Precision: 2, Rate: 35.0},
"JPY": {Code: "JPY", Symbol: "¥", Precision: 0, Rate: 150.0},
}

2. 价格存储

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE product (
sku_id BIGINT PRIMARY KEY,
name VARCHAR(255),
base_price_usd DECIMAL(12, 2) COMMENT '基准价格(USD)',
created_at TIMESTAMP
);

CREATE TABLE product_price_override (
sku_id BIGINT,
currency VARCHAR(3),
price DECIMAL(12, 2) COMMENT '覆盖价格(非汇率转换)',
PRIMARY KEY (sku_id, currency)
) COMMENT='特定币种的价格覆盖(如中国区特价)';

设计理念

  • 所有商品有USD基准价格
  • 特定币种可以覆盖价格(运营定价策略)

3. 实时汇率服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type ExchangeRateService struct {
redis RedisClient
external ExternalRateProvider // 第三方汇率API
}

func (s *ExchangeRateService) GetRate(from, to string) (float64, error) {
// 如果相同币种,汇率=1
if from == to {
return 1.0, nil
}

// 从Redis缓存查询(5分钟TTL)
cacheKey := fmt.Sprintf("exchange_rate:%s:%s", from, to)
if cached, err := s.redis.Get(cacheKey); err == nil {
return parseFloat(cached), nil
}

// 缓存未命中,调用第三方API
rate, err := s.external.GetRate(from, to)
if err != nil {
// 降级:使用静态配置的汇率
return s.getStaticRate(from, to), nil
}

// 写入缓存
s.redis.Set(cacheKey, fmt.Sprintf("%.6f", rate), 5*time.Minute)

return rate, nil
}

4. 价格计算逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func (s *PricingService) CalculatePrice(ctx context.Context, skuID int64, currency string, userID int64) (*Price, error) {
// Step 1: 查询商品基准价格(USD)
basePrice, err := s.productRepo.GetBasePriceUSD(skuID)
if err != nil {
return nil, err
}

// Step 2: 检查是否有币种覆盖价格
if override, err := s.productRepo.GetPriceOverride(skuID, currency); err == nil {
basePrice = override // 使用覆盖价格
} else {
// Step 3: 没有覆盖,使用汇率转换
rate, _ := s.exchangeRateService.GetRate("USD", currency)
basePrice = basePrice * rate
}

// Step 4: 应用促销(促销金额也需要币种转换)
promo, _ := s.marketingClient.GetPromotion(skuID, userID, currency)
discount := s.applyPromotion(basePrice, promo)

// Step 5: 按币种精度舍入
precision := SupportedCurrencies[currency].Precision
finalPrice := roundToPrecision(basePrice - discount, precision)

return &Price{
Amount: finalPrice,
Currency: currency,
Original: basePrice,
Discount: discount,
}, nil
}

5. 订单存储(保留汇率快照)

1
2
3
4
5
6
7
8
9
CREATE TABLE `order` (
order_id BIGINT PRIMARY KEY,
user_id BIGINT,
currency VARCHAR(3),
amount DECIMAL(12, 2),
amount_usd DECIMAL(12, 2) COMMENT 'USD等值金额(用于报表统计)',
exchange_rate DECIMAL(10, 6) COMMENT '创单时的汇率快照',
created_at TIMESTAMP
);

为什么存储汇率快照?

  • 财务审计需要:知道创单时的汇率
  • 退款场景:按创单时汇率退款(而非当前汇率)
  • 报表统计:统一转换为USD对比

6. 促销金额的币种处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 促销配置
type Promotion struct {
ID string
DiscountType string // "rate"(折扣率)/ "amount"(固定金额)

// 如果是固定金额,需要配置多币种
DiscountAmounts map[string]float64 // {"CNY": 50, "USD": 7, "SGD": 10}
}

// 应用促销
func (s *PricingService) applyPromotion(basePrice float64, promo *Promotion, currency string) float64 {
if promo.DiscountType == "rate" {
// 折扣率:与币种无关
return basePrice * (1 - promo.DiscountRate)
}

// 固定金额:查找对应币种的金额
if amount, ok := promo.DiscountAmounts[currency]; ok {
return basePrice - amount
}

// 如果没有配置该币种,转换USD金额
usdAmount := promo.DiscountAmounts["USD"]
rate, _ := s.exchangeRateService.GetRate("USD", currency)
return basePrice - (usdAmount * rate)
}

追问方向

  1. 汇率变动如何处理?用户看到的价格会变吗?

    • 实时汇率每5分钟更新
    • 用户试算时使用实时汇率
    • 创单时锁定汇率(生成订单)
    • 订单金额不受后续汇率变动影响
  2. 如果第三方汇率API失败怎么办?

    • 降级到静态配置的汇率(每日更新)
    • 告警通知SRE
    • 用户无感知(不影响下单)
  3. 跨币种退款如何处理?

    • 原路退回:按创单时汇率退款
    • 例如:创单时100 USD = 720 CNY(汇率7.2)
    • 退款时即使汇率变为7.0,仍退720 CNY
  4. 如何支持”中国区特价”这种运营策略?

    • 使用product_price_override
    • 运营配置CNY特价(不基于汇率转换)
    • 优先级:覆盖价格 > 汇率转换价格

答题要点

  • 多币种配置(精度、符号)
  • 基准币种+覆盖价格
  • 实时汇率服务
  • 订单汇率快照

加分项

  • 提及财务审计需求
  • 提及退款场景
  • 提及降级策略
  • 提及运营定价策略

Q16:如何设计价格计算的AB测试?

考察点:AB测试设计、实验平台、数据分析

参考答案

价格是电商的核心要素,任何价格策略调整(如促销规则、计算逻辑)都需要AB测试验证效果。

AB测试场景示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
实验1:满减门槛优化
- 对照组:满300减50
- 实验组:满200减30
- 目标:提升订单转化率

实验2:折扣展示方式
- 对照组:展示最终价格"¥269"
- 实验组:展示原价+折扣"¥299 ¥269(9折)"
- 目标:提升点击率

实验3:运费策略
- 对照组:满99元包邮
- 实验组:满59元包邮
- 目标:提升客单价

AB测试框架设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
type ABTestConfig struct {
ExperimentID string
Name string
Traffic float64 // 实验流量占比(0.1 = 10%)
Variants []*Variant
TargetMetrics []string // ["conversion_rate", "gmv", "aov"]
StartTime time.Time
EndTime time.Time
}

type Variant struct {
ID string // "control", "variant_a", "variant_b"
Name string
Traffic float64 // 该变体占实验流量的比例
Config map[string]interface{} // 实验配置
}

// 示例配置
abtest := &ABTestConfig{
ExperimentID: "EXP_001",
Name: "满减门槛优化",
Traffic: 0.2, // 20%总流量参与实验
Variants: []*Variant{
{
ID: "control",
Name: "对照组",
Traffic: 0.5, // 实验流量的50%
Config: map[string]interface{}{
"threshold": 300,
"reduce": 50,
},
},
{
ID: "variant_a",
Name: "实验组A",
Traffic: 0.5,
Config: map[string]interface{}{
"threshold": 200,
"reduce": 30,
},
},
},
TargetMetrics: []string{"conversion_rate", "gmv"},
StartTime: time.Now(),
EndTime: time.Now().Add(7 * 24 * time.Hour), // 运行7天
}

流量分配逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type ABTestService struct {
redis RedisClient
}

func (s *ABTestService) AssignVariant(userID int64, experimentID string) (*Variant, error) {
// Step 1: 查询实验配置
exp, err := s.getExperiment(experimentID)
if err != nil {
return nil, err
}

// Step 2: 判断用户是否进入实验
hash := fnv1a(fmt.Sprintf("%d:%s", userID, experimentID))
bucket := float64(hash%10000) / 10000.0 // 0.0000 - 0.9999

if bucket > exp.Traffic {
return nil, nil // 不参与实验
}

// Step 3: 分配变体(基于用户ID保证一致性)
variantBucket := float64(hash%1000) / 1000.0
accumulated := 0.0

for _, variant := range exp.Variants {
accumulated += variant.Traffic
if variantBucket < accumulated {
// 缓存用户的变体分配(实验期间不变)
s.redis.Set(
fmt.Sprintf("abtest:%s:user:%d", experimentID, userID),
variant.ID,
exp.EndTime.Sub(time.Now()))

return variant, nil
}
}

return exp.Variants[0], nil // fallback到对照组
}

价格计算中使用AB测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (s *PricingService) CalculatePrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
// Step 1: 查询用户所属的AB测试变体
variant, _ := s.abtestService.AssignVariant(req.UserID, "EXP_001")

// Step 2: 根据变体选择计算策略
var promos []*Promotion
if variant != nil {
// 使用实验配置
threshold := variant.Config["threshold"].(float64)
reduce := variant.Config["reduce"].(float64)

promos = []*Promotion{{
Type: "order_reduce",
Threshold: threshold,
Reduce: reduce,
}}
} else {
// 默认配置
promos = s.marketingClient.GetPromotions(req.SkuIDs, req.UserID)
}

// Step 3: 计算价格
price := s.calculate(req.Items, promos)

// Step 4: 记录实验数据
s.trackExperiment(req.UserID, "EXP_001", variant.ID, "price_calculated", price)

return price, nil
}

实验数据采集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type ExperimentEvent struct {
ExperimentID string
VariantID string
UserID int64
EventType string // "price_calculated", "order_created", "payment_completed"
Timestamp int64
Properties map[string]interface{}
}

func (s *PricingService) trackExperiment(userID int64, expID, variantID, eventType string, price *Price) {
event := &ExperimentEvent{
ExperimentID: expID,
VariantID: variantID,
UserID: userID,
EventType: eventType,
Timestamp: time.Now().Unix(),
Properties: map[string]interface{}{
"base_price": price.BasePrice,
"discount": price.Discount,
"final_price": price.FinalPrice,
},
}

// 异步写入Kafka(用于后续分析)
s.kafka.Publish("experiment.events", event)
}

实验效果分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 对照组 vs 实验组效果对比
SELECT
variant_id,
COUNT(DISTINCT user_id) AS users,
COUNT(DISTINCT CASE WHEN event_type = 'order_created' THEN user_id END) AS converted_users,
COUNT(DISTINCT CASE WHEN event_type = 'order_created' THEN user_id END) * 1.0 / COUNT(DISTINCT user_id) AS conversion_rate,
AVG(CASE WHEN event_type = 'payment_completed' THEN properties->>'amount' END) AS avg_order_value,
SUM(CASE WHEN event_type = 'payment_completed' THEN (properties->>'amount')::numeric END) AS total_gmv
FROM experiment_events
WHERE experiment_id = 'EXP_001'
AND timestamp BETWEEN '2026-04-01' AND '2026-04-08'
GROUP BY variant_id;

-- 结果示例:
-- variant_id | users | converted_users | conversion_rate | avg_order_value | total_gmv
-- control | 10000 | 1500 | 0.150 | 350.00 | 525000
-- variant_a | 10000 | 1800 | 0.180 | 280.00 | 504000
--
-- 结论:实验组转化率提升20%(0.180 vs 0.150),但GMV略降(客单价下降)

统计显著性检验

1
2
3
4
5
6
// 使用卡方检验判断转化率差异是否显著
func (s *ABTestService) CheckSignificance(controlConversion, variantConversion float64, sampleSize int) bool {
// 简化实现,实际应使用统计库
expectedDiff := 0.05 // 5%差异认为显著
return math.Abs(variantConversion-controlConversion) > expectedDiff
}

实验决策

1
2
3
4
5
6
7
8
9
实验结果:
• 实验组转化率提升20%(0.180 vs 0.150)✓
• 实验组GMV略降4%(504k vs 525k)❌
• 实验组客单价下降20%(280 vs 350)❌

决策:
• 如果目标是拉新(提升转化率)→ 采用实验组
• 如果目标是GMV最大化 → 继续使用对照组
• 可以考虑分场景:新用户用实验组,老用户用对照组

追问方向

  1. 如何保证同一用户始终看到相同的变体?

    • 基于用户ID哈希分桶(一致性哈希)
    • Redis缓存用户的变体分配
    • 实验期间分配不变
  2. 如果实验效果很差,如何快速止损?

    • 实时监控核心指标(转化率、GMV)
    • 设置止损阈值(如GMV下降>10%自动停止)
    • 一键回滚到对照组
  3. 如何避免辛普森悖论(Simpson’s Paradox)?

    • 分层分析(按用户等级、地域、品类)
    • 避免只看全局指标
    • 考虑混杂变量(如节假日、大促)
  4. AB测试与灰度发布有何区别?

    • AB测试:对比两种策略的效果,需要统计分析
    • 灰度发布:逐步放量新功能,验证稳定性
    • 灰度可以100%,AB测试通常小流量(10%-20%)

答题要点

  • AB测试框架设计
  • 流量分配(一致性哈希)
  • 实验数据采集
  • 统计显著性检验

加分项

  • 提及具体实验场景
  • 提及辛普森悖论
  • 提及止损机制
  • 提及AB测试vs灰度发布的区别

Q17:价格计算服务的性能指标有哪些?如何监控?

考察点:可观测性、SLI/SLO设计、监控体系

参考答案

价格计算服务是核心服务,必须建立完善的监控体系。

核心性能指标(SLI - Service Level Indicator)

1. 延迟指标

1
2
3
4
5
6
7
8
9
10
11
type LatencyMetrics struct {
P50 float64 // 中位数延迟
P95 float64 // 95分位延迟
P99 float64 // 99分位延迟
P999 float64 // 99.9分位延迟
}

// 目标SLO(Service Level Objective):
// - P95 < 200ms
// - P99 < 300ms
// - P999 < 500ms

2. 可用性指标

1
2
3
4
5
type AvailabilityMetrics struct {
SuccessRate float64 // 成功率(目标:>99.9%)
ErrorRate float64 // 错误率(目标:<0.1%)
Uptime float64 // 可用时间占比
}

3. 吞吐量指标

1
2
3
4
5
6
type ThroughputMetrics struct {
QPS float64 // 每秒查询数
QPM float64 // 每分钟查询数
PeakQPS float64 // 峰值QPS
DailyTotal int64 // 日总请求数
}

4. 业务指标

1
2
3
4
5
6
7
8
9
10
11
12
type BusinessMetrics struct {
// 缓存相关
CacheHitRate float64 // 缓存命中率(目标:>80%)
SnapshotHitRate float64 // 快照命中率(目标:>80%)

// 计算准确性
PriceDifferenceRate float64 // 新老系统差异率(目标:<0.01%)

// 资源使用
AvgCalculationTime float64 // 平均计算耗时
BatchSize float64 // 平均批量大小
}

监控实现

方案1:Prometheus + Grafana

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
// 延迟直方图
priceCalculationDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "pricing_calculation_duration_seconds",
Help: "Price calculation duration in seconds",
Buckets: prometheus.DefBuckets, // [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
},
[]string{"method", "status"},
)

// QPS计数器
priceCalculationTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "pricing_calculation_total",
Help: "Total number of price calculations",
},
[]string{"method", "status"},
)

// 缓存命中率
cacheHitRate = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "pricing_cache_hit_rate",
Help: "Cache hit rate",
},
[]string{"cache_type"},
)
)

func (s *PricingService) Calculate(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
startTime := time.Now()

defer func() {
duration := time.Since(startTime).Seconds()
priceCalculationDuration.WithLabelValues("calculate", "success").Observe(duration)
priceCalculationTotal.WithLabelValues("calculate", "success").Inc()
}()

// 计算逻辑...

return result, nil
}

Grafana Dashboard示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─────────────────────────────────────────────────────────────────┐
│ Pricing Service Overview │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│
│ │QPS: 1,850 ││P95: 185ms ││Success Rate:││Cache Hit: ││
│ │ ││ ││99.95% ││82% ││
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘│
├─────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────── Latency (P95/P99) ─────────────────┐│
│ │ ││
│ │ 300ms ┤ ││
│ │ │ ╱╲ ││
│ │ 200ms ┤───────────╱──╲────────────────────────────────── ││
│ │ │ ╱ ╲ ││
│ │ 100ms ┤─────────╱──────╲──────────────────────────────── ││
│ │ │ ││
│ │ 0ms └───────────────────────────────────────────────── ││
│ │ 00:00 06:00 12:00 18:00 00:00 ││
│ └──────────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────── QPS Trend ─────────────────────────┐│
│ │ ││
│ │ 3000 ┤ ╱╲ ││
│ │ │ ╱ ╲ ││
│ │ 2000 ┤──────────────────────────╱────╲─────────────────── ││
│ │ │ ╱ ╲ ││
│ │ 1000 ┤────────────────────────╱────────╲───────────────── ││
│ │ │ ││
│ │ 0 └─────────────────────────────────────────────────── ││
│ │ 00:00 06:00 12:00 18:00 00:00 ││
│ └──────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

方案2:自定义监控指标上报

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
type MetricsCollector struct {
metrics chan *Metric
}

type Metric struct {
Name string
Value float64
Tags map[string]string
Timestamp int64
}

func (c *MetricsCollector) RecordLatency(method string, duration time.Duration) {
c.metrics <- &Metric{
Name: "pricing.latency",
Value: duration.Seconds() * 1000, // 转为ms
Tags: map[string]string{"method": method},
Timestamp: time.Now().Unix(),
}
}

func (c *MetricsCollector) RecordCacheHit(cacheType string, hit bool) {
value := 0.0
if hit {
value = 1.0
}

c.metrics <- &Metric{
Name: "pricing.cache.hit",
Value: value,
Tags: map[string]string{"cache_type": cacheType},
Timestamp: time.Now().Unix(),
}
}

// 后台goroutine批量上报
func (c *MetricsCollector) Start() {
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

buffer := make([]*Metric, 0, 1000)

for {
select {
case metric := <-c.metrics:
buffer = append(buffer, metric)

if len(buffer) >= 1000 {
c.flush(buffer)
buffer = buffer[:0]
}

case <-ticker.C:
if len(buffer) > 0 {
c.flush(buffer)
buffer = buffer[:0]
}
}
}
}()
}

func (c *MetricsCollector) flush(metrics []*Metric) {
// 批量上报到监控系统(如Datadog、Prometheus Pushgateway)
c.monitoringClient.BatchReport(metrics)
}

告警规则配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Prometheus alerting rules
groups:
- name: pricing_service_alerts
interval: 30s
rules:
# P99延迟告警
- alert: PricingHighLatency
expr: histogram_quantile(0.99, pricing_calculation_duration_seconds) > 0.3
for: 5m
labels:
severity: warning
team: pricing
annotations:
summary: "Pricing服务P99延迟过高"
description: "P99延迟{{ $value }}秒,超过300ms阈值"

# 错误率告警
- alert: PricingHighErrorRate
expr: |
sum(rate(pricing_calculation_total{status="error"}[5m]))
/
sum(rate(pricing_calculation_total[5m])) > 0.01
for: 2m
labels:
severity: critical
team: pricing
annotations:
summary: "Pricing服务错误率过高"
description: "错误率{{ $value | humanizePercentage }},超过1%阈值"

# 缓存命中率告警
- alert: PricingLowCacheHitRate
expr: pricing_cache_hit_rate < 0.7
for: 10m
labels:
severity: warning
team: pricing
annotations:
summary: "Pricing服务缓存命中率过低"
description: "缓存命中率{{ $value | humanizePercentage }},低于70%"

追问方向

  1. 如何设定合理的SLO?

    • 基于历史数据(P95/P99)
    • 基于业务需求(用户可接受延迟)
    • 基于竞品对比
    • 逐步迭代(先宽松后收紧)
  2. P99延迟突然升高,如何快速定位问题?

    • 查看Grafana Dashboard(是否有流量突增)
    • 查看Jaeger链路追踪(哪个依赖服务变慢)
    • 查看应用日志(是否有ERROR日志)
    • 查看系统指标(CPU、内存、GC)
  3. 如何监控缓存命中率?

    • 每次查询记录是否命中
    • 按缓存类型分组(L1本地/L2 Redis/快照)
    • 实时计算5分钟滑动窗口命中率
    • 低于阈值告警
  4. 监控数据如何存储?保留多久?

    • Prometheus:15天高精度(15s间隔)
    • 长期存储(Thanos):90天低精度(5min间隔)
    • ES日志:30天

答题要点

  • SLI/SLO设计
  • Prometheus + Grafana监控
  • 告警规则配置
  • 链路追踪(Jaeger)

加分项

  • 提及具体SLO目标(P95<200ms)
  • 提及告警分级(warning/critical)
  • 提及监控数据保留策略
  • 提及问题定位流程

Q18:价格计算引擎如何支持A/B测试不同的促销算法?

考察点:扩展性设计、策略模式、配置化能力

参考答案

价格计算引擎需要支持多种促销算法的A/B测试,验证哪种算法效果更好。

设计理念:策略模式 + 配置化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 促销计算策略接口
type PromotionStrategy interface {
Name() string
Calculate(basePrice float64, items []*Item, promo *Promotion) float64
}

// 策略1:传统满减算法
type TraditionalReduceStrategy struct{}

func (s *TraditionalReduceStrategy) Name() string {
return "traditional_reduce"
}

func (s *TraditionalReduceStrategy) Calculate(basePrice float64, items []*Item, promo *Promotion) float64 {
if basePrice >= promo.Threshold {
return promo.ReduceAmount
}
return 0
}

// 策略2:阶梯满减算法
type TieredReduceStrategy struct{}

func (s *TieredReduceStrategy) Name() string {
return "tiered_reduce"
}

func (s *TieredReduceStrategy) Calculate(basePrice float64, items []*Item, promo *Promotion) float64 {
// 满200减20,满300减50,满500减100
if basePrice >= 500 {
return 100
} else if basePrice >= 300 {
return 50
} else if basePrice >= 200 {
return 20
}
return 0
}

// 策略3:动态折扣算法(金额越高折扣越大)
type DynamicDiscountStrategy struct{}

func (s *DynamicDiscountStrategy) Name() string {
return "dynamic_discount"
}

func (s *DynamicDiscountStrategy) Calculate(basePrice float64, items []*Item, promo *Promotion) float64 {
// 200-300: 5%折扣
// 300-500: 10%折扣
// 500+: 15%折扣
if basePrice >= 500 {
return basePrice * 0.15
} else if basePrice >= 300 {
return basePrice * 0.10
} else if basePrice >= 200 {
return basePrice * 0.05
}
return 0
}

策略工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type PromotionStrategyFactory struct {
strategies map[string]PromotionStrategy
}

func NewPromotionStrategyFactory() *PromotionStrategyFactory {
factory := &PromotionStrategyFactory{
strategies: make(map[string]PromotionStrategy),
}

// 注册所有策略
factory.Register(&TraditionalReduceStrategy{})
factory.Register(&TieredReduceStrategy{})
factory.Register(&DynamicDiscountStrategy{})

return factory
}

func (f *PromotionStrategyFactory) Register(strategy PromotionStrategy) {
f.strategies[strategy.Name()] = strategy
}

func (f *PromotionStrategyFactory) Get(name string) PromotionStrategy {
if strategy, ok := f.strategies[name]; ok {
return strategy
}
return &TraditionalReduceStrategy{} // 默认策略
}

AB测试集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (s *PricingService) Calculate(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
// Step 1: 查询用户所属的AB测试变体
variant, _ := s.abtestService.AssignVariant(req.UserID, "PROMO_ALGO_TEST")

// Step 2: 根据变体选择促销策略
strategyName := "traditional_reduce" // 默认策略
if variant != nil {
strategyName = variant.Config["strategy"].(string)
}

strategy := s.strategyFactory.Get(strategyName)

// Step 3: 使用选定的策略计算
basePrice := calculateSubtotal(req.Items)
discount := strategy.Calculate(basePrice, req.Items, req.Promotion)
finalPrice := basePrice - discount

// Step 4: 记录实验数据
s.trackExperiment(req.UserID, "PROMO_ALGO_TEST", variant.ID, strategy.Name(), finalPrice)

return &PriceResponse{
FinalPrice: finalPrice,
Discount: discount,
Strategy: strategy.Name(),
}, nil
}

AB测试配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"experiment_id": "PROMO_ALGO_TEST",
"name": "促销算法A/B测试",
"traffic": 0.2,
"variants": [
{
"id": "control",
"name": "传统满减",
"traffic": 0.33,
"config": {
"strategy": "traditional_reduce"
}
},
{
"id": "variant_a",
"name": "阶梯满减",
"traffic": 0.33,
"config": {
"strategy": "tiered_reduce"
}
},
{
"id": "variant_b",
"name": "动态折扣",
"traffic": 0.34,
"config": {
"strategy": "dynamic_discount"
}
}
],
"target_metrics": ["conversion_rate", "gmv", "aov"]
}

实验效果分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 各策略效果对比
SELECT
strategy_name,
COUNT(DISTINCT user_id) AS users,
COUNT(CASE WHEN event_type = 'order_created' THEN 1 END) AS orders,
AVG(CASE WHEN event_type = 'payment_completed' THEN amount END) AS avg_order_value,
SUM(CASE WHEN event_type = 'payment_completed' THEN amount END) AS total_gmv
FROM experiment_events
WHERE experiment_id = 'PROMO_ALGO_TEST'
GROUP BY strategy_name;

-- 结果示例:
-- strategy_name | users | orders | avg_order_value | total_gmv
-- traditional_reduce | 10000 | 1500 | 350.00 | 525000
-- tiered_reduce | 10000 | 1650 | 380.00 | 627000 ← 最佳
-- dynamic_discount | 10000 | 1600 | 360.00 | 576000

追问方向

  1. 如何快速增加新的促销策略?

    • 实现PromotionStrategy接口
    • 注册到工厂
    • 配置AB测试
    • 无需修改核心计算逻辑
  2. 如何保证策略切换的一致性?

    • 基于用户ID哈希分配策略
    • 实验期间用户策略不变
    • Redis缓存用户分配结果
  3. 如果某个策略有严重bug,如何紧急下线?

    • 修改AB测试配置,将该变体流量设为0
    • 配置实时生效(无需重启服务)
    • 流量立即切到对照组

答题要点

  • 策略模式
  • 策略工厂
  • AB测试集成
  • 配置化设计

加分项

  • 提及设计模式(Strategy Pattern)
  • 提及扩展性设计
  • 提及实验数据分析
  • 提及紧急下线机制

主题二:快照机制与缓存策略(15题)

2.1 快照设计(ADR-008)

Q19:为什么需要快照机制?解决了什么问题?(ADR-008)

考察点:架构决策理解、性能优化思维、用户体验设计

参考答案

快照机制是我们架构中的核心设计之一(ADR-008),它解决了性能与准确性的平衡问题

没有快照机制的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户行为:商品详情页 → 加购 → 结算页 → 提交订单

❌ 方案1:每次都实时查询(无缓存)
• 详情页:查询Product + Marketing + Inventory(3次RPC,150ms)
• 加购:再次查询(3次RPC,150ms)
• 结算页:再次查询(3次RPC,150ms)
• 问题:重复查询浪费资源,用户体验差

❌ 方案2:长时间缓存(30分钟TTL)
• 详情页:写入Redis缓存,30分钟有效
• 结算页:使用缓存数据
• 问题:缓存期间商品可能下架、促销失效,导致资损

❌ 方案3:前端缓存
• 详情页:前端缓存商品数据
• 结算页:使用前端缓存
• 问题:前端数据可篡改,安全风险高

快照机制的设计(ADR-008):

1
2
3
4
5
6
7
8
9
10
11
12
13
type Snapshot struct {
ID string // 快照ID(UUID)
Data map[string]interface{} // 快照数据
CreatedAt int64 // 创建时间
ExpiresAt int64 // 过期时间(CreatedAt + 5分钟)
}

// 快照数据结构
type SnapshotData struct {
Product *Product // 商品信息
Promotion *Promotion // 营销信息
Price *PriceInfo // 价格信息(计算好的)
}

快照机制的三个关键设计

1. 客户端存储,服务端校验

1
2
3
4
5
6
7
8
9
10
11
详情页(Phase 0):
↓ 生成快照ID + 快照数据
客户端存储快照ID
↓ 用户点击"立即购买"
结算页(Phase 2):
↓ 传入快照ID
服务端校验快照是否过期
↓ 如果未过期,使用快照数据(性能优化)✓
↓ 如果已过期,重新查询(准确性保证)✓
创单(Phase 3):
↓ 强制实时校验(不使用快照)✓

2. 短TTL(5分钟)

1
2
3
4
5
6
7
8
为什么是5分钟?
• 太短(1分钟):命中率低,性能优化效果差
• 太长(30分钟):数据陈旧风险高
• 5分钟:平衡点(用户从详情页到结算的中位数时长:2-3分钟)

监控数据(生产环境):
• 快照命中率:82%(即82%用户在5分钟内完成结算)
• 未命中原因:用户犹豫时间过长、促销活动失效

3. 创单强制实时校验

1
2
3
4
5
6
7
8
9
10
即使快照有效,创单时也必须实时查询最新数据:
• 商品是否下架
• 促销是否失效
• 库存是否充足
• 价格是否变化

如果数据有变化:
• 提示用户:"价格已变化,当前价格为X元,是否继续?"
• 用户重新确认
• 保证用户最终支付的价格是准确的

快照的生成与使用(文档4.2节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Phase 0: 商品详情页生成快照
func (s *ProductService) GetProductDetail(ctx context.Context, skuID int64) (*ProductDetail, error) {
// Step 1: 查询商品、促销、价格
product := s.productClient.GetProduct(skuID)
promo := s.marketingClient.GetPromotion(skuID)
price := s.pricingClient.Calculate(product, promo)

// Step 2: 生成快照
snapshot := &Snapshot{
ID: generateSnapshotID(), // UUID
Data: map[string]interface{}{
"product": product,
"promotion": promo,
"price": price,
},
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(5 * time.Minute).Unix(),
}

// Step 3: 写入Redis(5分钟TTL)
s.redis.Set(fmt.Sprintf("snapshot:%s", snapshot.ID), serialize(snapshot), 5*time.Minute)

// Step 4: 返回详情页数据 + 快照ID
return &ProductDetail{
Product: product,
Promotion: promo,
Price: price,
SnapshotID: snapshot.ID, // 前端存储这个ID
}, nil
}

// Phase 2: 结算页使用快照
func (s *CheckoutService) Calculate(ctx context.Context, req *CalculateRequest) (*CalculateResponse, error) {
var needQuerySkuIDs []int64
snapshotData := make(map[int64]*SnapshotData)

// Step 1: 判断快照是否过期
for _, item := range req.Items {
if item.SnapshotID != "" {
snapshot, err := s.redis.Get(fmt.Sprintf("snapshot:%s", item.SnapshotID))
if err == nil && snapshot.ExpiresAt > time.Now().Unix() {
// 快照有效,使用快照数据
snapshotData[item.SkuID] = snapshot.Data
} else {
// 快照过期,需要重新查询
needQuerySkuIDs = append(needQuerySkuIDs, item.SkuID)
}
} else {
needQuerySkuIDs = append(needQuerySkuIDs, item.SkuID)
}
}

// Step 2: 只查询未命中快照的SKU(性能优化)
if len(needQuerySkuIDs) > 0 {
products := s.productClient.BatchGetProducts(needQuerySkuIDs)
promos := s.marketingClient.BatchGetPromotions(needQuerySkuIDs)
// 合并到snapshotData
}

// Step 3: 使用snapshotData计算价格
// 注意:库存必须实时查询(不使用快照)
stocks := s.inventoryClient.BatchCheckStock(allSkuIDs)

return calculatePrice(snapshotData, stocks), nil
}

快照机制的收益

指标 无快照 有快照 提升
结算页P95延迟 350ms 230ms 34%↓
RPC调用次数 3次/请求 0.54次/请求 82%↓
Redis QPS 0 +1500/s -
MySQL QPS 6000/s 1080/s 82%↓
快照命中率 - 82% -

追问方向

  1. 为什么快照只在试算阶段使用,创单必须实时查询?

    • 试算:性能优先,允许轻微延迟(5分钟内数据)
    • 创单:准确性优先,必须强一致性(防止资损)
    • 即使试算使用了过期快照,创单时的实时校验会拦截所有不一致
  2. 快照ID是如何生成的?为什么不用SKU ID?

    • 使用UUID(唯一性)
    • 包含用户ID + SKU ID + 时间戳的哈希
    • 不能用SKU ID:同一商品不同用户/时间的快照内容不同(促销、价格)
  3. 如果快照在Redis中丢失(如Redis故障),如何处理?

    • 视为快照过期,重新查询
    • 用户无感知(无错误提示)
    • 降级到无快照模式(性能稍慢,但功能正常)
  4. 快照机制与HTTP缓存(ETag)有何区别?

    • 快照:业务层缓存,包含完整数据,5分钟有效
    • ETag:HTTP层缓存,只缓存静态资源(图片、CSS)
    • 快照解决的是动态数据(价格、库存)的缓存问题

答题要点

  • 性能与准确性的平衡
  • 客户端存储+服务端校验
  • 5分钟TTL(用户行为分析)
  • 创单强制实时校验

加分项

  • 提及具体性能数据(P95延迟降34%)
  • 提及快照命中率(82%)
  • 提及降级策略(Redis故障)
  • 提及防御性设计(创单强制校验)

Q20:快照数据存储在哪里?Redis还是前端?

考察点:架构选型、安全性、性能权衡

参考答案

快照数据采用混合存储策略:快照ID由前端存储,快照数据由Redis存储。

方案对比

方案 优点 缺点 采纳
前端存储(LocalStorage/SessionStorage) 无服务端压力 数据可篡改,安全风险高
Redis存储 数据安全,服务端可控 占用Redis空间
MySQL存储 数据持久化 性能差,浪费存储

实际设计(文档ADR-008):

1
2
前端:存储快照ID(snapshot_id: "abc-123-def")
Redis:存储快照数据(key: snapshot:abc-123-def, value: {...})

为什么不在前端存储完整快照数据?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 坏的设计:前端存储完整数据
localStorage.setItem('snapshot', JSON.stringify({
product: {...}, // 可篡改:改价格、改库存
price: 299.00, // 可篡改:改成1元
promotion: {...} // 可篡改:伪造促销
}));

// 用户篡改价格
let snapshot = JSON.parse(localStorage.getItem('snapshot'));
snapshot.price = 1.00; // 篡改为1元
localStorage.setItem('snapshot', JSON.stringify(snapshot));

// 提交订单时传给服务端
// 如果服务端信任前端数据 → 资损

Redis存储设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 快照Key设计
key := fmt.Sprintf("snapshot:%s", snapshotID)

// 快照Value(JSON)
{
"snapshot_id": "abc-123-def",
"sku_id": 1001,
"data": {
"product": {
"sku_id": 1001,
"name": "商品名称",
"base_price": 299.00
},
"promotion": {
"promo_id": "P001",
"discount_rate": 0.9
},
"price": {
"original": 299.00,
"final": 269.10,
"discount": 29.90
}
},
"created_at": 1713091200,
"expires_at": 1713091500 // 5分钟后过期
}

// TTL: 5分钟(300秒)

前端使用流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Step 1: 商品详情页获取快照ID
fetch('/product/detail?sku_id=1001')
.then(res => res.json())
.then(data => {
// 存储快照ID到SessionStorage(会话级别,关闭浏览器失效)
sessionStorage.setItem('snapshot_id_1001', data.snapshot_id);

// 渲染页面
renderProductDetail(data);
});

// Step 2: 用户点击"立即购买",跳转到结算页
window.location.href = '/checkout?sku_id=1001';

// Step 3: 结算页传入快照ID
const snapshotID = sessionStorage.getItem('snapshot_id_1001');

fetch('/checkout/calculate', {
method: 'POST',
body: JSON.stringify({
items: [{sku_id: 1001, quantity: 1, snapshot_id: snapshotID}]
})
});

// 服务端根据snapshot_id从Redis查询快照数据,校验并使用

Redis容量估算

1
2
3
4
5
6
7
8
9
单个快照大小:约2KB(JSON)
并发用户:10万(同时浏览商品)
快照命中率:80%(20%用户超过5分钟)

Redis存储容量:
= 10万用户 × 2KB × 20%(未过期的)
= 40MB

实际部署:Redis Cluster 8主8从,每主节点内存64GB,绰绰有余

追问方向

  1. 如果用户篡改快照ID(传入别人的快照ID)会怎样?

    • 快照数据不包含敏感信息(不含用户优惠券、Coin)
    • 篡改快照ID只能看到别人的商品价格(无危害)
    • 创单时强制实时校验用户身份和权限
  2. Redis故障导致快照丢失怎么办?

    • 服务降级:视为快照过期,重新查询
    • 用户体验稍差(多一次RPC),但功能正常
    • Redis主从+Sentinel保证高可用
  3. 为什么用SessionStorage而不是LocalStorage?

    • SessionStorage:会话级别,关闭浏览器失效(更合理)
    • LocalStorage:永久存储,可能导致用户看到旧快照ID

Q21:三级缓存架构如何设计?

考察点:缓存架构设计、多级缓存、性能优化

参考答案(文档3.5节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
L1: 本地缓存(Application Memory)
├─ 容量:100MB
├─ TTL:1分钟
├─ 命中率:60%
├─ 延迟:<1ms
└─ 适用:热点商品基础信息

L2: Redis缓存(Distributed Cache)
├─ 容量:64GB×8节点
├─ TTL:5-30分钟
├─ 命中率:95%(含L1)
├─ 延迟:1-3ms
└─ 适用:商品、价格、促销

L3: MySQL(Source of Truth)
├─ 容量:TB级
├─ 延迟:50-100ms
└─ 权威数据源

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
type ThreeTierCache struct {
l1Cache *LocalCache // 本地缓存
l2Cache *RedisClient // Redis缓存
db *MySQLClient // MySQL数据库
}

func (c *ThreeTierCache) GetProduct(skuID int64) (*Product, error) {
// L1: 本地缓存查询
cacheKey := fmt.Sprintf("product:%d", skuID)
if val, ok := c.l1Cache.Get(cacheKey); ok {
c.metrics.RecordCacheHit("L1")
return val.(*Product), nil
}

// L2: Redis查询
if val, err := c.l2Cache.Get(cacheKey); err == nil {
product := deserialize(val)

// 写入L1(1分钟TTL)
c.l1Cache.Set(cacheKey, product, 1*time.Minute)

c.metrics.RecordCacheHit("L2")
return product, nil
}

// L3: MySQL查询
product, err := c.db.QueryProduct(skuID)
if err != nil {
return nil, err
}

// 写入L2(30分钟TTL)
c.l2Cache.Set(cacheKey, serialize(product), 30*time.Minute)

// 写入L1(1分钟TTL)
c.l1Cache.Set(cacheKey, product, 1*time.Minute)

c.metrics.RecordCacheHit("L3_miss")
return product, nil
}

缓存策略对比

数据类型 L1 TTL L2 TTL 原因
商品基础信息 5分钟 30分钟 变化频率低
商品价格 1分钟 5分钟 促销可能变化
库存 不缓存 不缓存 实时性要求高
促销活动 1分钟 5分钟 变化频率中
用户Coin 不缓存 不缓存 涉及资金

Q22:缓存一致性如何保证?

考察点:缓存一致性、数据同步、事件驱动

参考答案

采用Cache-Aside + 主动失效策略。

更新策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 更新商品价格
func (s *ProductService) UpdatePrice(skuID int64, newPrice float64) error {
// Step 1: 更新MySQL
if err := s.db.UpdatePrice(skuID, newPrice); err != nil {
return err
}

// Step 2: 删除Redis缓存(让其自然重建)
s.redis.Del(fmt.Sprintf("product:%d", skuID))

// Step 3: 发布Kafka事件
s.kafka.Publish("product.price.updated", &Event{
SkuID: skuID,
NewPrice: newPrice,
})

return nil
}

// 订阅事件并删除本地缓存
func (s *PricingService) OnPriceUpdated(event *Event) {
s.localCache.Del(fmt.Sprintf("product:%d", event.SkuID))
}

为什么删除缓存而不是更新缓存?

1
2
3
4
5
6
7
8
9
删除缓存(推荐):
• MySQL更新 → 删除Redis → 下次查询时重建
• 优点:简单,不会出现数据不一致
• 缺点:首次查询慢(缓存未命中)

更新缓存(不推荐):
• MySQL更新 → 更新Redis
• 缺点:如果Redis更新失败,数据不一致
• 缺点:并发更新可能导致顺序错乱

主题三:营销系统设计(12题 - 精简版)

Q23:如何设计营销活动的预扣(Reserve)机制?

考察点:资源预占、2PC、并发控制

参考答案(文档ADR-011):

营销资源(优惠券、Coin)采用Reserve-Confirm两阶段提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Phase 1: 试算阶段 - 只校验,不预扣
func (s *MarketingService) ValidateCoupon(couponID string, userID int64) (*Coupon, error) {
coupon := s.repo.GetCoupon(couponID)

// 校验:用户是否拥有、是否过期、是否已使用
if coupon.UserID != userID {
return nil, fmt.Errorf("优惠券不属于该用户")
}

if coupon.Status != CouponStatusUnused {
return nil, fmt.Errorf("优惠券已使用")
}

return coupon, nil
}

// Phase 2: 创单阶段 - 预扣(Reserve)
func (s *MarketingService) ReserveCoupon(couponID string, orderID int64) (string, error) {
// CAS更新:status = UNUSED → RESERVED
affected, err := s.db.Exec(`
UPDATE coupon
SET status = 'RESERVED', reserve_order_id = ?, reserve_at = NOW()
WHERE coupon_id = ? AND status = 'UNUSED'
`, orderID, couponID)

if affected == 0 {
return "", fmt.Errorf("优惠券已被使用")
}

// 返回预占ID
return fmt.Sprintf("reserve_%s", couponID), nil
}

// Phase 3: 支付成功 - 确认(Confirm)
func (s *MarketingService) ConfirmCoupon(reserveID string) error {
// RESERVED → USED
s.db.Exec(`UPDATE coupon SET status = 'USED' WHERE reserve_id = ?`, reserveID)
return nil
}

// Phase 4: 支付失败/订单取消 - 释放(Release)
func (s *MarketingService) ReleaseCoupon(reserveID string) error {
// RESERVED → UNUSED
s.db.Exec(`UPDATE coupon SET status = 'UNUSED', reserve_order_id = NULL WHERE reserve_id = ?`, reserveID)
return nil
}

状态机

1
2
3
4
5
UNUSED(未使用)
↓ Reserve
RESERVED(预占中)
├─ Confirm → USED(已使用)
└─ Release → UNUSED(释放)

Q24:如何防止营销活动被刷单/薅羊毛?

考察点:风控设计、防刷机制、限流策略

参考答案

多层防护机制。

1. 用户维度限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 每个用户每天最多领取3张优惠券
type CouponQuota struct {
UserID int64
PromoID string
DailyMax int // 每日上限
UsedToday int // 今日已领取
}

func (s *MarketingService) ClaimCoupon(userID int64, promoID string) error {
quota := s.getQuota(userID, promoID)

if quota.UsedToday >= quota.DailyMax {
return fmt.Errorf("今日领取次数已达上限")
}

// 发放优惠券...

// 增加计数(Redis INCR)
s.redis.Incr(fmt.Sprintf("coupon:quota:%d:%s:%s", userID, promoID, today()))
s.redis.Expire(key, 24*time.Hour)

return nil
}

2. IP/设备限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 同一IP每分钟最多领取10张券
func (s *MarketingService) CheckRateLimit(ip string) error {
key := fmt.Sprintf("ratelimit:coupon:ip:%s", ip)
count := s.redis.Incr(key)

if count == 1 {
s.redis.Expire(key, 1*time.Minute)
}

if count > 10 {
return fmt.Errorf("操作过于频繁")
}

return nil
}

3. 风控规则引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type RiskRule struct {
Name string
Condition func(user *User, order *Order) bool
Action string // "block"/"alert"/"review"
}

var rules = []RiskRule{
{
Name: "新注册用户大额订单",
Condition: func(u *User, o *Order) bool {
return time.Since(u.CreatedAt) < 24*time.Hour && o.Amount > 1000
},
Action: "review",
},
{
Name: "短时间内多次下单",
Condition: func(u *User, o *Order) bool {
recentOrders := getRecentOrders(u.UserID, 10*time.Minute)
return len(recentOrders) > 5
},
Action: "block",
},
}

主题四:库存设计与超卖防护(15题 - 精简版)

Q25:二维库存模型是什么?为什么这样设计?

考察点:领域模型设计、业务抽象

参考答案(文档5.1节):

电商库存有两个维度:管理类型(ManagementType)单位类型(UnitType)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type InventoryModel struct {
SkuID int64
ManagementType ManagementType // 库存管理方式
UnitType UnitType // 库存单位
Quantity int64 // 库存数量
}

// 管理类型
type ManagementType int
const (
ManagementTypeReal ManagementType = 1 // 实物库存(需扣减)
ManagementTypeVirtual ManagementType = 2 // 虚拟库存(无限)
ManagementTypeOnDemand ManagementType = 3 // 按需生成(供应商确认)
)

// 单位类型
type UnitType int
const (
UnitTypePiece UnitType = 1 // 件(如手机)
UnitTypeCard UnitType = 2 // 卡券(如充值卡)
UnitTypeQuantity UnitType = 3 // 份额(如话费充值)
)

为什么需要二维模型?

商品类型 ManagementType UnitType 扣减逻辑
实物商品(手机) Real Piece 下单扣减,取消释放
虚拟卡券(充值卡) Real Card 下单扣减(卡号唯一)
数字商品(话费充值) Virtual Quantity 不扣减(供应商无限)
酒店房间 OnDemand Piece 下单确认后扣减

Q26:Redis Lua脚本如何实现原子扣减?

考察点:Redis原子操作、Lua脚本、并发安全

参考答案(文档5.2.2节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- inventory_reserve.lua
local key = KEYS[1] -- inventory:sku:1001
local quantity = tonumber(ARGV[1]) -- 扣减数量

local available = tonumber(redis.call('HGET', key, 'available'))

if available == nil or available < quantity then
return -1 -- 库存不足
end

-- 原子扣减
redis.call('HINCRBY', key, 'available', -quantity)
redis.call('HINCRBY', key, 'reserved', quantity)

return 1 -- 成功

Go调用

1
2
3
4
5
6
7
8
9
10
11
12
13
func (s *InventoryService) ReserveStock(skuID int64, quantity int64) error {
script := `...` // 上面的Lua脚本

result, err := s.redis.Eval(script,
[]string{fmt.Sprintf("inventory:sku:%d", skuID)},
quantity)

if result == -1 {
return fmt.Errorf("库存不足")
}

return nil
}

为什么用Lua而不是WATCH/MULTI?

1
2
Lua脚本:原子执行,不会被其他命令打断 ✓
WATCH/MULTI:乐观锁,高并发下重试多次,性能差 ❌

主题五至八:支撑主题(精简合并)

由于篇幅限制,我将剩余主题(分布式事务、高并发、容错、微服务)合并为精华版,每个主题3-4题:

主题五:分布式事务与一致性(4题精简)

Q27:Saga模式如何实现分布式事务?

参考答案(文档6.1节):

1
2
3
4
5
// 正向流程
CreateOrder() → ReserveInventory() → ReserveCoupon() → CreatePayment()

// 补偿流程(任一步骤失败触发)
CancelPayment() → ReleaseCoupon() → ReleaseInventory() → CancelOrder()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) error {
saga := NewSaga()

// Step 1: 创建订单
orderID, err := s.orderRepo.Create(req)
saga.AddCompensation(func() { s.orderRepo.Cancel(orderID) })
if err != nil {
saga.Rollback()
return err
}

// Step 2: 预占库存
reserveID, err := s.inventoryClient.Reserve(req.Items)
saga.AddCompensation(func() { s.inventoryClient.Release(reserveID) })
if err != nil {
saga.Rollback()
return err
}

// Step 3: 预扣优惠券
couponReserveID, err := s.marketingClient.ReserveCoupon(req.CouponID)
saga.AddCompensation(func() { s.marketingClient.ReleaseCoupon(couponReserveID) })
if err != nil {
saga.Rollback()
return err
}

return nil
}

Q28:如何保证接口幂等性?

参考答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
type IdempotencyService struct {
redis RedisClient
}

func (s *IdempotencyService) Execute(idempotencyKey string, fn func() (interface{}, error)) (interface{}, error) {
// 检查是否已执行
if result, err := s.redis.Get(fmt.Sprintf("idempotency:%s", idempotencyKey)); err == nil {
return deserialize(result), nil // 返回缓存结果
}

// 加分布式锁(防止并发重复执行)
lock := s.redis.Lock(fmt.Sprintf("idempotency:lock:%s", idempotencyKey), 10*time.Second)
if !lock.Acquire() {
return nil, fmt.Errorf("请求处理中,请勿重复提交")
}
defer lock.Release()

// 再次检查(double-check)
if result, err := s.redis.Get(fmt.Sprintf("idempotency:%s", idempotencyKey)); err == nil {
return deserialize(result), nil
}

// 执行业务逻辑
result, err := fn()
if err != nil {
return nil, err
}

// 缓存结果(24小时)
s.redis.Set(fmt.Sprintf("idempotency:%s", idempotencyKey), serialize(result), 24*time.Hour)

return result, nil
}

// 使用示例
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
return s.idempotency.Execute(req.IdempotencyKey, func() (interface{}, error) {
// 实际创建订单逻辑
return s.createOrderInternal(ctx, req)
})
}

主题六:高并发与性能优化(4题精简)

Q29:如何应对大促流量洪峰?

参考答案(文档8.3节):

1. 提前扩容

  • Kubernetes HPA:CPU>70%自动扩容
  • 大促前手动扩容:Checkout Service 10 → 50 pods

2. 限流保护

1
2
3
4
5
6
7
8
9
10
// 令牌桶限流
limiter := rate.NewLimiter(1000, 2000) // 1000 QPS,最大burst 2000

func (s *CheckoutService) Calculate(ctx context.Context, req *Request) (*Response, error) {
if !limiter.Allow() {
return nil, fmt.Errorf("系统繁忙,请稍后重试")
}

return s.calculateInternal(ctx, req)
}

3. 降级策略

1
2
3
4
5
// 营销服务降级:失败返回空促销
promos, err := s.marketingClient.GetPromotions(skuIDs)
if err != nil {
promos = []*Promotion{} // 降级:无促销
}

4. 缓存预热

  • 大促前预热Top 1000商品价格缓存

Q30:分库分表如何设计?

参考答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 订单表按user_id分库分表(8库×8表=64张表)
func (r *OrderRepository) getShardKey(userID int64) (db int, table int) {
db = int(userID % 8) // 分库
table = int((userID / 8) % 8) // 分表
return
}

func (r *OrderRepository) GetOrder(orderID int64, userID int64) (*Order, error) {
db, table := r.getShardKey(userID)

query := fmt.Sprintf(`
SELECT * FROM order_%d
WHERE order_id = ? AND user_id = ?
`, table)

return r.dbs[db].QueryRow(query, orderID, userID)
}

路由规则

  • 订单表:按user_id分片(保证单用户订单在同一分片,便于查询)
  • 商品表:按sku_id分片
  • 库存表:按sku_id分片

主题七:系统容错与稳定性(4题精简)

Q31:熔断降级如何实现?

参考答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import "github.com/sony/gobreaker"

var cb = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "MarketingService",
MaxRequests: 3, // 半开状态最多3个请求
Interval: 10 * time.Second, // 统计周期
Timeout: 60 * time.Second, // 熔断后60秒恢复到半开状态
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 3 && failureRatio >= 0.6 // 失败率>60%触发熔断
},
})

func (s *CheckoutService) Calculate(ctx context.Context, req *Request) (*Response, error) {
// 通过熔断器调用
result, err := cb.Execute(func() (interface{}, error) {
return s.marketingClient.GetPromotions(req.SkuIDs)
})

if err == gobreaker.ErrOpenState {
// 熔断打开,降级处理
return s.calculateWithoutPromotion(req)
}

return s.calculateWithPromotion(req, result.([]*Promotion))
}

状态机

1
2
3
4
5
6
7
CLOSED(关闭,正常) 
↓ 失败率>60%
OPEN(打开,拒绝请求)
↓ 60秒后
HALF_OPEN(半开,允许少量请求)
├─ 成功 → CLOSED
└─ 失败 → OPEN

Q32:如何保证服务高可用(99.95%)?

参考答案(文档9.1节):

1. 多副本部署

  • 每个微服务至少3个副本
  • 分布在不同节点/可用区

2. 健康检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Kubernetes Liveness Probe
func (s *Server) LivenessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}

// Kubernetes Readiness Probe
func (s *Server) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
// 检查依赖服务是否可用
if !s.redis.Ping() {
w.WriteHeader(http.StatusServiceUnavailable)
return
}

if !s.db.Ping() {
w.WriteHeader(http.StatusServiceUnavailable)
return
}

w.WriteHeader(http.StatusOK)
}

3. 同城双活

  • 部署在同城2个数据中心(DC1、DC2)
  • MySQL双主同步
  • Redis Cluster跨DC部署

主题八:微服务架构与部署(4题精简)

Q33:聚合服务的职责是什么?

参考答案(文档2.4节):

聚合服务负责数据编排和聚合,简化前端调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
前端直接调用各微服务(❌不推荐):
Frontend → Product Service
→ Marketing Service
→ Inventory Service
→ Pricing Service
(4次HTTP请求,前端逻辑复杂)

通过聚合服务(✅推荐):
Frontend → Aggregation Service
├→ Product Service
├→ Marketing Service (并发调用)
├→ Inventory Service
└→ Pricing Service
(1次HTTP请求,后端聚合数据)

代码示例(文档2.4节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func (s *AggregationService) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
// Step 1: ES搜索
skuIDs := s.esClient.Search(req.Keyword)

// Step 2: 并发调用各服务
var wg sync.WaitGroup
var products []*Product
var stocks map[int64]*Stock
var promos map[int64]*Promotion

wg.Add(3)
go func() {
products = s.productClient.BatchGet(skuIDs)
wg.Done()
}()

go func() {
stocks = s.inventoryClient.BatchCheck(skuIDs)
wg.Done()
}()

go func() {
promos = s.marketingClient.BatchGetPromotions(skuIDs)
wg.Done()
}()

wg.Wait()

// Step 3: 串行调用Pricing(依赖products + promos)
prices := s.pricingClient.BatchCalculate(products, promos)

// Step 4: 聚合数据
return s.buildSearchResponse(products, stocks, promos, prices)
}

Q34:如何进行灰度发布?

参考答案(文档9.3节):

基于Kubernetes的灰度发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 灰度版本Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-service-v2
spec:
replicas: 2 # 灰度版本2个副本
selector:
matchLabels:
app: checkout-service
version: v2
template:
metadata:
labels:
app: checkout-service
version: v2
spec:
containers:
- name: checkout-service
image: checkout-service:v2.0.0

---

# Service(权重路由)
apiVersion: v1
kind: Service
metadata:
name: checkout-service
spec:
selector:
app: checkout-service # 同时选中v1和v2
ports:
- port: 80
targetPort: 8080

---

# Istio VirtualService(流量分配)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: checkout-service
spec:
hosts:
- checkout-service
http:
- match:
- headers:
x-user-id:
regex: ".*[02468]$" # 尾号为偶数的用户
route:
- destination:
host: checkout-service
subset: v2
weight: 100 # 灰度版本
- route:
- destination:
host: checkout-service
subset: v1
weight: 100 # 稳定版本

灰度策略

  • Week 1: 10%流量(尾号为0的用户)
  • Week 2: 50%流量(尾号为偶数的用户)
  • Week 3: 100%流量

模拟面试场景(3个完整场景)

场景一:订单创建全流程设计

面试官:请设计一个完整的订单创建流程,从用户点击”提交订单”到订单创建成功,需要考虑哪些关键问题?

答题框架

1. 数据流设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
用户提交订单(Phase 3b)

① 校验商品信息(Product Service)
- 商品是否下架
- 价格是否有效

② 校验营销活动(Marketing Service)
- 促销是否有效
- 优惠券是否可用

③ 预占库存(Inventory Service)
- CAS原子扣减
- 生成预占ID

④ 预扣营销资源(Marketing Service)
- 优惠券标记为RESERVED
- Coin冻结

⑤ 计算最终价格(Pricing Service)
- 基于最新数据实时计算
- 比对快照价格(允许误差0.01元)

⑥ 创建订单(Order Service)
- 写入订单主表
- 记录价格明细
- 状态:PENDING_PAYMENT

返回订单ID和支付信息

2. 关键技术点

  • 幂等性:基于idempotency_key防重
  • 分布式事务:Saga模式,任一步骤失败触发补偿
  • 并发安全:库存CAS扣减、优惠券CAS预扣
  • 性能优化:批量接口、并发调用
  • 可观测性:全链路追踪(Jaeger)、价格明细记录

3. 异常处理

异常场景 处理方式
商品下架 提示用户,拒绝创单
促销失效 提示”活动已结束,当前价格XXX”
库存不足 提示”库存不足”,释放已预占资源
优惠券已用 提示”优惠券已使用”
价格变化>1元 提示”价格已变化”,用户重新确认

4. 性能指标

  • P95延迟:< 500ms
  • 成功率:> 99.9%
  • 并发:5000 QPS(大促10000 QPS)

场景二:秒杀系统设计

面试官:双11秒杀iPhone,库存100台,预计10万人抢购,如何设计?

答题框架

1. 流量削峰

1
2
3
4
5
6
7
8
9
API Gateway限流:10000 QPS

前端页面排队(虚拟等待室)

服务端限流(令牌桶):1000 QPS

Redis预占库存

MySQL确认扣减

2. Redis预占设计

1
2
3
4
5
6
7
8
9
10
-- 秒杀预占Lua脚本
local key = "seckill:sku:1001"
local stock = tonumber(redis.call('GET', key))

if stock == nil or stock <= 0 then
return -1 -- 已抢完
end

redis.call('DECR', key)
return 1 -- 抢到了

3. 防刷措施

  • 用户限购:1人最多抢1台
  • 验证码:防机器人
  • 风控规则:新注册账号不能参与

4. 库存同步

1
2
3
Redis库存预占成功
↓ 异步MQ
MySQL扣减库存(最终一致性)

5. 性能保障

  • 提前预热缓存
  • 数据库连接池扩容
  • HPA自动扩容(50 → 200 pods)

场景三:价格故障定位

面试官:用户投诉”我看到的价格是250元,为什么支付时变成300元?”,如何快速定位?

答题步骤

1. 查询订单价格明细

1
2
3
SELECT breakdown_json 
FROM order_price_breakdown
WHERE order_id = 123456;

2. 分析PriceBreakdown

1
2
3
4
5
6
7
8
9
10
11
12
{
"base_price": 300.00,
"promotion_details": [
{
"promo_id": "P001",
"promo_name": "限时折扣",
"applied": false,
"reason": "促销库存已用尽"
}
],
"final_price": 300.00
}

3. 查询快照数据

1
2
3
4
5
6
7
8
9
10
11
12
# 用户看到250元时的快照
redis-cli GET snapshot:abc-123-def

{
"price": 250.00, # 快照价格
"promotion": {
"promo_id": "P001",
"status": "ACTIVE" # 快照时促销有效
},
"created_at": 1713091200,
"expires_at": 1713091500
}

4. 查询促销历史

1
2
3
4
5
SELECT * FROM promotion_history 
WHERE promo_id = 'P001'
AND timestamp BETWEEN 1713091200 AND 1713091800;

-- 发现:促销在1713091300(快照后100秒)库存用尽

5. 结论

1
2
3
4
5
6
7
8
9
10
11
12
13
用户在详情页看到价格时(12:00:00):
• 促销P001有效
• 快照价格250元

5分钟后用户创单时(12:05:00):
• 促销P001库存已用尽(12:01:40失效)
• 系统正确:拒绝使用失效促销
• 实际价格300元

建议:
• 前端提示"活动火爆,价格可能变化,以实际支付为准"
• 缩短快照TTL(5分钟→3分钟)
• 促销库存预警(剩余10%时提示)

总结

本面试题库涵盖B2B2C电商系统的8大核心主题,共70+道题目,适合Staff/Principal Engineer级别的面试准备。每个问题都包含考察点、参考答案、追问方向、答题要点和加分项,帮助你系统性地理解和掌握大型电商系统的架构设计。

使用建议

  1. 按主题逐个攻克,重点准备标星(⭐⭐⭐)主题
  2. 结合文档4.1-9.4节的详细设计深入理解
  3. 模拟面试场景进行练习
  4. 关注答题要点和加分项,展示架构思维

祝面试顺利!🎉