电商架构演进与商品系统(SPU/SKU)
电商架构演进路线
电商业务从诞生至今,其技术架构经历了四次重大演进,每一次演进都为了解决前一阶段的核心痛点。
单体架构(LAMP/SSH 时代)
早期电商网站(如 2000 年代初期)多采用 LAMP(Linux + Apache + MySQL + PHP)或 SSH(Spring + Struts + Hibernate)单体架构。所有业务模块——用户、商品、订单、支付、库存——打包在同一个部署单元中。
┌─────────────────────────────────────┐
│ 单体应用 (WAR/JAR) │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 用户 │ │ 商品 │ │ 订单 │ │
│ │ 模块 │ │ 模块 │ │ 模块 │ │
│ └──────┘ └──────┘ └──────┘ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 支付 │ │ 库存 │ │ 营销 │ │
│ │ 模块 │ │ 模块 │ │ 模块 │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ Apache / Nginx │ │
│ └──────────────────────┘ │
└─────────────────────────────────────┘
│
┌─────────────┴─────────────┐
│ 共享数据库 MySQL │
│ (所有表在同一个库中) │
└───────────────────────────┘优点:
- 开发简单,技术栈统一,IDE 支持好
- 部署便捷,一个 WAR/JAR 包即可运行
- 调试方便,本地即可完成全链路测试
- 调用延迟低,方法调用在 JVM 进程内完成
痛点分析:
| 痛点类型 | 表现 | 后果 |
|---|---|---|
| 代码耦合 | 订单模块直接调用用户模块 DAO | 无法独立开发、测试、部署 |
| 扩展粒度粗 | 流量集中在商品模块,却必须整体扩容 | 资源浪费严重,扩容成本高 |
| 团队协作困难 | 多人修改同一代码库,Git 冲突频繁 | 发布周期长,上线风险高 |
| 技术栈锁定 | 全部使用 Java/Spring,无法引入异构技术 | 无法选择最适合特定场景的技术 |
| 稳定性差 | 一个模块的内存泄漏拖垮整个应用 | 故障隔离性为零 |
垂直拆分(按业务模块分库)
随着业务规模扩大,团队规模增长,单体架构的耦合问题日益突出。架构师开始按业务模块将应用垂直拆分为多个子系统。
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 用户系统 │ │ 商品系统 │ │ 订单系统 │ │ 支付系统 │
│ (独立部署) │ │ (独立部署) │ │ (独立部署) │ │ (独立部署) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 用户库 DB │ │ 商品库 DB │ │ 订单库 DB │ │ 支付库 DB │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
┌──────────────────────────────────────────────────┐
│ HTTP / RPC 服务间调用 │
│ (Dubbo / Spring Cloud / HTTP Client) │
└──────────────────────────────────────────────────┘优点:
- 业务解耦,各团队独立负责各自的子系统
- 数据库拆分,单一数据库压力降低
- 可针对热点业务独立扩容(如商品系统部署多台)
- 故障隔离,一个系统宕机不影响其他系统
痛点分析:
- 系统间通信引入网络开销,延迟增加
- 分布式事务处理复杂(如下单扣库存的一致性保证)
- 服务调用关系混乱,形成「蜘蛛网」依赖
- 缺少统一的服务治理(熔断、限流、降级)
- 重复代码增多,各系统各自实现相同功能(如用户鉴权)
SOA / 微服务(业务中台化)
垂直拆分的进一步演进引入了中台化思想,将各业务线的公共能力下沉到中台服务层,同时服务治理能力逐步完善。
┌─────────────────────────────────────────────────────────────────┐
│ 前台业务层(Frontend) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ PC 端 │ │ 移动端 │ │ 小程序 │ │ 开放平台 │ │
│ └─────┬────┘ └─────┬────┘ └─────┬────┘ └─────┬────┘ │
└────────┼────────────┼────────────┼────────────┼──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ API 网关层(Gateway) │
│ 路由转发 / 认证鉴权 / 限流熔断 / 协议转换 │
└──────────────┬────────────────────────────────┬─────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌──────────────────────────────────┐
│ 业务中台服务层 │ │ 技术中台 / 基础设施 │
│ ┌──────┐ ┌──────┐ │ │ ┌──────┐ ┌──────┐ │
│ │商品 │ │订单 │ │ │ │注册 │ │配置 │ │
│ │中心 │ │中心 │ │ │ │中心 │ │中心 │ │
│ ├──────┤ ├──────┤ │ │ ├──────┤ ├──────┤ │
│ │支付 │ │库存 │ │ │ │链路 │ │日志 │ │
│ │中心 │ │中心 │ │ │ │追踪 │ │平台 │ │
│ ├──────┤ ├──────┤ │ │ ├──────┤ ├──────┤ │
│ │用户 │ │营销 │ │ │ │监控 │ │CI/CD │ │
│ │中心 │ │中心 │ │ │ │告警 │ │ │ │
│ ├──────┤ ├──────┤ │ │ └──────┘ └──────┘ │
│ │搜索 │ │履约 │ │ └──────────────────────────────────┘
│ │推荐 │ │中心 │ │
│ └──────┘ └──────┘ │
└─────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 数据存储层 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │MySQL │ │Redis │ │ES │ │MQ │ │MongoDB│ │
│ │(关系) │ │(缓存) │ │(搜索) │ │(消息) │ │(文档) │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
└─────────────────────────────────────────────────────────────────┘核心能力(服务治理):
- 服务注册与发现:Nacos / Eureka / Consul,动态管理服务地址
- 远程调用:Dubbo / OpenFeign / gRPC,支持负载均衡
- 熔断降级:Sentinel / Hystrix / Resilience4j,防止级联故障
- API 网关:Spring Cloud Gateway / Kong / APISIX,统一入口
- 分布式事务:Seata(AT / TCC / Saga)+ 消息最终一致性
- 配置中心:Nacos Config / Apollo / Spring Cloud Config,配置动态推送
优点:
- 服务独立部署、独立扩展、独立演进
- 中台能力复用,避免重复建设
- 异构技术栈支持,不同服务可选最合适的技术
- 精细化服务治理,保障系统稳定性
痛点分析:
- 微服务拆分粒度难以把握,过度拆分导致运维成本激增
- 分布式系统固有的复杂性(网络延迟、数据一致性、调试困难)
- 服务间调用链路过长,性能损耗不可忽视
- 基础设施投入大(注册中心、配置中心、监控系统等)
云原生(容器化 + 服务网格)
云原生是微服务的进一步升级,通过容器化、Kubernetes 编排和服务网格,将基础设施管理自动化,让开发者专注于业务逻辑。
┌─────────────────────────────────────────────────────────────────────────┐
│ Kubernetes 集群 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Istio 服务网格 (Service Mesh) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Pilot │ │ Mixer │ │ Citadel │ │ Galley │ │ │
│ │ │ (服务发现)│ │ (策略) │ │ (安全) │ │ (配置) │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Pod │ │ Pod │ │ Pod │ │ Pod │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │商品 │ │ │ │订单 │ │ │ │支付 │ │ │ │库存 │ │ │
│ │ │服务 │ │ │ │服务 │ │ │ │服务 │ │ │ │服务 │ │ │
│ │ ├──────┤ │ │ ├──────┤ │ │ ├──────┤ │ │ ├──────┤ │ │
│ │ │Envoy │ │ │ │Envoy │ │ │ │Envoy │ │ │ │Envoy │ │ │
│ │ │Proxy │ │ │ │Proxy │ │ │ │Proxy │ │ │ │Proxy │ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 云原生基础设施 │ │
│ │ HPA(弹性伸缩) ConfigMap(配置) Secret(密钥) PVC(持久化) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ DevOps 工具链 │ │
│ │ Docker(容器) Jenkins/GitLab(CI) ArgoCD(CD) Helm(包管理) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘核心特点:
| 维度 | 传统微服务 | 云原生 |
|---|---|---|
| 部署单元 | JAR/WAR 包部署在虚拟机 | Docker 容器,镜像化交付 |
| 编排调度 | 手动管理 / 自建脚本 | Kubernetes 自动编排 |
| 弹性伸缩 | 手动扩缩容 / 定时策略 | HPA 基于指标自动伸缩 |
| 服务治理 | SDK 侵入式(如 Sentinel jar 包) | Sidecar 代理(Envoy)无侵入 |
| 发布策略 | 滚动更新 | 蓝绿部署 / 金丝雀发布 / 灰度发布 |
| 可观测性 | 埋点 + 日志 + 指标拼凑 | OpenTelemetry 统一标准 |
| 基础设施 | 自建机房 / IDC | 云平台(AWS / 阿里云 / 腾讯云) |
痛点解决情况:
- 运维自动化:K8s 自愈、自动扩缩容,大幅降低运维成本
- 流量治理:Istio 通过 Sidecar 劫持流量,无需修改业务代码即可实现熔断、限流、重试
- 安全通信:mTLS 自动加密服务间通信,Citadel 管理证书生命周期
- 可观测性:Envoy 自动上报链路追踪和指标,配合 Prometheus + Grafana + Kiali
遗留挑战:
- 技术栈复杂度陡增,团队学习曲线陡峭
- 资源开销增加(Sidecar 代理引入额外延迟和资源占用)
- 排障链路变长(需要理解 K8s、Istio、业务代码多层)
电商中台架构概览
电商中台的核心是将各业务线的公共能力下沉为中台服务,形成「大中台、小前台」的组织和技术架构。以下为各大服务中心的核心职责。
服务中心职责矩阵
| 服务中心 | 核心职责 | 关键能力 |
|---|---|---|
| 商品中心 | 商品 SPU/SKU 管理、类目管理、属性管理、品牌管理、商品审核 | 商品发布、上下架、多规格、版本管理 |
| 订单中心 | 订单创建、订单状态流转、订单拆分、售后处理 | 状态机管理、逆向流程、OMS |
| 支付中心 | 支付渠道对接、支付路由、对账、退款 | 多支付渠道聚合、支付安全、分账 |
| 库存中心 | 库存占用、释放、锁定、预扣 | 分布式库存、多渠道库存、安全库存 |
| 用户中心 | 用户注册登录、账号管理、收货地址、会员等级 | SSO 统一认证、会员体系 |
| 营销中心 | 优惠券、促销活动、秒杀、拼团 | 营销规则引擎、风控、实时计价 |
| 搜索推荐 | 商品搜索、类目导航、个性化推荐 | ES 搜索引擎、推荐算法、排序策略 |
| 履约中心 | 仓储调度、物流配送、配送时效预估 | WMS 对接、智能分仓、轨迹追踪 |
中台服务间协作示例(商品搜索流程)
用户 → API 网关 → 搜索推荐服务 → (检索 ES) → 返回商品列表
│
└→ 调用商品中心 → 获取商品详情 / 实时价格 / 库存
│
└→ 调用营销中心 → 计算优惠 / 标签
│
└→ 调用库存中心 → 查询实时库存商品 SPU / SKU 模型
SPU 与 SKU 概念详解
SPU(Standard Product Unit,标准化产品单元):描述一个抽象的商品概念,是商品信息聚合的最小单位。例如:"iPhone 15 Pro Max" 是一个 SPU,它代表了苹果公司的一款产品型号,不区分颜色、内存等具体规格。
SKU(Stock Keeping Unit,库存量单位):库存进出计量的最小单元,是可以实际购买的具体商品。例如:"iPhone 15 Pro Max / 原色钛金属 / 256GB" 就是一个 SKU。
两者关系示意:
SPU: iPhone 15 Pro Max
┌───────────┬───────────┐
│ │ │
SKU-1 SKU-2 SKU-3
原色钛金属/256GB 原色钛金属/512GB 白色钛金属/256GB
¥9999 ¥11999 ¥9999
库存: 1200 库存: 800 库存: 950SPU 与 SKU 的核心区别
| 维度 | SPU | SKU |
|---|---|---|
| 定义层级 | 抽象产品概念 | 具体可售商品 |
| 唯一标识 | spu_id | sku_id |
| 价格 | 价格区间(如 ¥8999~¥11999) | 具体定价 |
| 库存 | 不直接管理库存 | 管理具体库存数量 |
| 规格属性 | 不包含销售属性具体值 | 包含销售属性具体值组合 |
| 购买操作 | 用户不能直接购买 SPU | 用户实际下单支付的是 SKU |
| 典型场景 | 商品详情页展示 | 购物车 + 下单 + 支付 |
类目体系
电商系统中通常采用两级或三级类目体系,核心原则是前台类目面向用户展示,后台类目面向管理绑定属性。
┌──────────────────┐
│ 后台类目(一级) │
│ 如:手机 / 电脑 / │
│ 服装 / 食品 │
└────────┬─────────┘
│ 包含关系
┌────────┴─────────┐
│ 后台类目(二级) │
│ 如:智能手机 / 功能机 │
└────────┬─────────┘
│ 关联关系
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 前台类目 A │ │ 前台类目 B │ │ 前台类目 C │
│ (首页推荐) │ │ (搜索结果) │ │ (活动专题) │
└──────────────┘ └──────────────┘ └──────────────┘后台类目(也称管理类目):
- 面向运营和管理人员,用于商品发布时的类目选择和属性绑定
- 结构相对稳定,一般三层结构(一级 > 二级 > 三级)
- 每个后台类目关联固定的属性模板
前台类目(也称展示类目):
- 面向消费者,用于前端导航和浏览
- 结构灵活可配,可以根据运营需要动态调整
- 一个后台类目可以映射到多个前台类目(如"智能手机"同时出现在"手机馆"和"数码节")
属性体系
商品属性体系
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
关键属性 销售属性 非关键属性
(品牌/型号/材质) (颜色/尺寸/容量) (重量/保修期/产地)
│
▼
自定义属性
(商家自定义扩展)关键属性(Critical Attributes):
- 用于唯一确定一个 SPU 的属性集合
- 例如:智能手机的关键属性为「品牌 + 型号」
- 一个关键属性组合对应一个 SPU
销售属性(Sales Attributes):
- 用于区分同一个 SPU 下不同 SKU 的属性
- 例如:颜色、内存容量、尺寸规格
- 销售属性的笛卡尔积生成 SKU 规格组合
非关键属性(Non-critical Attributes):
- 描述商品特征的辅助属性,不参与 SKU 区分
- 例如:净重、包装尺寸、上市日期
自定义属性(Custom Attributes):
- 商家在发布商品时根据所售商品特征自行添加的属性
- 例如:某手机商家添加「是否支持 5G」「充电功率」
多规格组合原理
多规格组合即多个销售属性值的笛卡尔积。以服装商品为例:
销售属性 A:颜色 = { 黑色, 白色, 蓝色 }
销售属性 B:尺码 = { S, M, L, XL }
笛卡尔积生成 SKU 规格:
(黑色, S) (黑色, M) (黑色, L) (黑色, XL)
(白色, S) (白色, M) (白色, L) (白色, XL)
(蓝色, S) (蓝色, M) (蓝色, L) (蓝色, XL)
共 3 × 4 = 12 个 SKU对于不存在的组合(如某颜色没有对应尺码),系统应支持手动禁用某组规格,不生成对应的 SKU。
商品数据库设计
商品主表(spu)
CREATE TABLE `spu` (
`spu_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'SPU ID',
`category_id` BIGINT NOT NULL COMMENT '后台类目 ID',
`brand_id` BIGINT DEFAULT NULL COMMENT '品牌 ID',
`name` VARCHAR(200) NOT NULL COMMENT '商品名称',
`subtitle` VARCHAR(500) DEFAULT NULL COMMENT '副标题 / 卖点',
`description` TEXT COMMENT '商品描述(富文本)',
`unit` VARCHAR(10) DEFAULT '件' COMMENT '销售单位',
`main_images` JSON COMMENT '商品主图列表',
`video_url` VARCHAR(500) DEFAULT NULL COMMENT '商品视频',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-草稿 1-待审核 2-审核通过 3-审核驳回 4-上架 5-下架',
`audit_status` TINYINT NOT NULL DEFAULT 0 COMMENT '审核状态: 0-未提交 1-审核中 2-通过 3-驳回',
`audit_remark` VARCHAR(500) DEFAULT NULL COMMENT '审核备注',
`sale_type` TINYINT NOT NULL DEFAULT 1 COMMENT '销售类型: 1-普通 2-预售 3-定制',
`is_multi_spec` TINYINT NOT NULL DEFAULT 0 COMMENT '是否多规格: 0-单规格 1-多规格',
`price_min` DECIMAL(10,2) DEFAULT NULL COMMENT '最低价格(冗余字段)',
`price_max` DECIMAL(10,2) DEFAULT NULL COMMENT '最高价格(冗余字段)',
`total_stock` INT DEFAULT 0 COMMENT '总库存(冗余字段)',
`sales_count` INT DEFAULT 0 COMMENT '销量(冗余字段)',
`version` INT NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删 1-已删',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`spu_id`),
KEY `idx_category` (`category_id`, `status`),
KEY `idx_brand` (`brand_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品 SPU 表';SKU 表(sku)
CREATE TABLE `sku` (
`sku_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'SKU ID',
`spu_id` BIGINT NOT NULL COMMENT '所属 SPU ID',
`sku_name` VARCHAR(300) NOT NULL COMMENT 'SKU 名称(如 iPhone 15 Pro Max 原色钛金属 256GB)',
`price` DECIMAL(10, 2) NOT NULL COMMENT '售价',
`market_price` DECIMAL(10, 2) DEFAULT NULL COMMENT '市场价 / 划线价',
`cost_price` DECIMAL(10, 2) DEFAULT NULL COMMENT '成本价',
`stock` INT NOT NULL DEFAULT 0 COMMENT '库存数量',
`freeze_stock` INT NOT NULL DEFAULT 0 COMMENT '冻结库存(已下单未支付)',
`saleable_stock` INT GENERATED ALWAYS AS (stock - freeze_stock) STORED COMMENT '可售库存(虚拟列)',
`weight` DECIMAL(10, 2) DEFAULT NULL COMMENT '重量(kg)',
`images` JSON COMMENT 'SKU 图片(规格相关图片,JSON 数组)',
`sales_attrs` JSON NOT NULL COMMENT '销售属性组合,如 [{"attr_id":1,"value":"黑色"},{"attr_id":2,"value":"256GB"}]',
`barcode` VARCHAR(100) DEFAULT NULL COMMENT '条码',
`outer_sku_id` VARCHAR(100) DEFAULT NULL COMMENT '外部 SKU 编码(对接 ERP)',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用 1-启用',
`sales_count` INT NOT NULL DEFAULT 0 COMMENT '销量',
`version` INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`sku_id`),
KEY `idx_spu` (`spu_id`),
KEY `idx_barcode` (`barcode`),
KEY `idx_price` (`price`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU 库存量单位表';类目表(category)
CREATE TABLE `category` (
`category_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '类目 ID',
`parent_id` BIGINT NOT NULL DEFAULT 0 COMMENT '父类目 ID(0 表示一级类目)',
`name` VARCHAR(100) NOT NULL COMMENT '类目名称',
`level` TINYINT NOT NULL COMMENT '层级: 1-一级 2-二级 3-三级',
`icon` VARCHAR(500) DEFAULT NULL COMMENT '类目图标',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序值(越小越靠前)',
`type` TINYINT NOT NULL DEFAULT 0 COMMENT '类型: 0-后台类目 1-前台类目',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用 1-启用',
`deleted` TINYINT NOT NULL DEFAULT 0,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`category_id`),
KEY `idx_parent` (`parent_id`, `level`),
KEY `idx_type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='类目表';
-- 前台类目与后台类目映射表
CREATE TABLE `category_mapping` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`front_cat_id` BIGINT NOT NULL COMMENT '前台类目 ID',
`back_cat_id` BIGINT NOT NULL COMMENT '后台类目 ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_mapping` (`front_cat_id`, `back_cat_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='前后台类目映射表';属性与属性值表
-- 属性定义表
CREATE TABLE `attribute` (
`attr_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '属性 ID',
`category_id` BIGINT NOT NULL COMMENT '所属后台类目 ID',
`attr_name` VARCHAR(100) NOT NULL COMMENT '属性名称',
`attr_type` TINYINT NOT NULL COMMENT '属性类型: 1-关键属性 2-销售属性 3-非关键属性 4-自定义属性',
`input_type` TINYINT NOT NULL DEFAULT 1 COMMENT '录入方式: 1-下拉选择 2-手动输入 3-多选',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`required` TINYINT NOT NULL DEFAULT 0 COMMENT '是否必填: 0-非必填 1-必填',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用 1-启用',
`deleted` TINYINT NOT NULL DEFAULT 0,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`attr_id`),
KEY `idx_category` (`category_id`),
KEY `idx_type` (`attr_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='属性定义表';
-- 属性值表
CREATE TABLE `attr_value` (
`value_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '属性值 ID',
`attr_id` BIGINT NOT NULL COMMENT '属性 ID',
`attr_value` VARCHAR(200) NOT NULL COMMENT '属性值(如"黑色"、"256GB")',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`status` TINYINT NOT NULL DEFAULT 1,
`deleted` TINYINT NOT NULL DEFAULT 0,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`value_id`),
KEY `idx_attr` (`attr_id`),
UNIQUE KEY `uk_attr_value` (`attr_id`, `attr_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='属性值表';
-- SPU 属性值关联表(存储 SPU 的关键属性和非关键属性值)
CREATE TABLE `spu_attr_value` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`spu_id` BIGINT NOT NULL COMMENT 'SPU ID',
`attr_id` BIGINT NOT NULL COMMENT '属性 ID',
`value_id` BIGINT DEFAULT NULL COMMENT '属性值 ID(下拉选择时)',
`attr_value` VARCHAR(500) DEFAULT NULL COMMENT '属性值(手动输入时或自定义属性)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_spu_attr` (`spu_id`, `attr_id`),
KEY `idx_attr_value` (`attr_id`, `value_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SPU 属性值关联表';商品详情存储
商品详情采用独立存储 + 多图/视频分离的设计模式:
CREATE TABLE `spu_detail` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`spu_id` BIGINT NOT NULL COMMENT 'SPU ID',
`detail_html` MEDIUMTEXT COMMENT '商品详情 HTML(富文本编辑器生成)',
`detail_images` JSON COMMENT '详情图片列表(JSON 数组)',
`detail_videos` JSON COMMENT '详情视频列表(JSON 数组,含封面图)',
`after_sale_desc` TEXT COMMENT '售后说明',
`packing_list` VARCHAR(500) DEFAULT NULL COMMENT '包装清单',
`version` INT NOT NULL DEFAULT 1 COMMENT '版本号',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_spu` (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品详情扩展表';商品搜索与缓存
ES 商品索引设计
商品搜索使用 Elasticsearch 作为检索引擎,索引设计采用 SPU + SKU 扁平化 策略,一个 ES 文档对应一个 SPU,内部嵌套 SKU 列表。
import co.elastic.clients.elasticsearch._types.mapping.*;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.elasticsearch.indices.putIndexTemplate.*;
import java.util.List;
// ES 索引映射定义(Java 伪代码 + DSL 映射)
public class ProductIndexDefinition {
public static final String INDEX_NAME = "product_index_v1";
public static final String INDEX_ALIAS = "product_index";
/**
* 创建商品索引映射
*/
public static PutIndexTemplateRequest buildMapping() {
return new PutIndexTemplateRequest.Builder()
.name("product_template")
.indexPatterns(List.of(INDEX_NAME))
.template(new IndexTemplateMapping.Builder()
.mappings(new TypeMapping.Builder()
.properties("spu_id", Property.of(p -> p.long_(v -> v)))
.properties("name", Property.of(p -> p.text(v -> v
.analyzer("ik_max_word")
.searchAnalyzer("ik_smart")
.fields("keyword", f -> f.keyword(k -> k)))))
.properties("subtitle", Property.of(p -> p.text(v -> v.analyzer("ik_max_word"))))
.properties("category_id", Property.of(p -> p.long_(v -> v)))
.properties("category_path", Property.of(p -> p.keyword(v -> v)))
.properties("brand_id", Property.of(p -> p.long_(v -> v)))
.properties("brand_name", Property.of(p -> p.keyword(v -> v)))
.properties("price_min", Property.of(p -> p.double_(v -> v)))
.properties("price_max", Property.of(p -> p.double_(v -> v)))
.properties("sales_count", Property.of(p -> p.long_(v -> v)))
.properties("status", Property.of(p -> p.byte_(v -> v)))
.properties("main_images", Property.of(p -> p.keyword(v -> v)))
.properties("attrs", Property.of(p -> p.nested(v -> v
.properties("attr_id", Property.of(pp -> pp.long_(k -> k)))
.properties("attr_name", Property.of(pp -> pp.keyword(k -> k)))
.properties("attr_value", Property.of(pp -> pp.keyword(k -> k)))
.properties("attr_type", Property.of(pp -> pp.byte_(k -> k)))
)))
.properties("skus", Property.of(p -> p.nested(v -> v
.properties("sku_id", Property.of(pp -> pp.long_(k -> k)))
.properties("price", Property.of(pp -> pp.double_(k -> k)))
.properties("stock", Property.of(pp -> pp.long_(k -> k)))
.properties("sales_attrs", Property.of(pp -> pp.keyword(k -> k)))
.properties("images", Property.of(pp -> pp.keyword(k -> k)))
)))
.properties("sale_type", Property.of(p -> p.byte_(v -> v)))
.properties("create_time", Property.of(p -> p.date(v -> v)))
.properties("update_time", Property.of(p -> p.date(v -> v)))
.build())
.build())
.build();
}
}搜索 DSL 示例(按关键词 + 类目 + 价格筛选):
{
"query": {
"bool": {
"must": [
{ "match": { "name": "iPhone 15" } }
],
"filter": [
{ "term": { "category_id": 123 } },
{ "range": { "price_min": { "gte": 5000 } } },
{ "term": { "status": 4 } }
]
}
},
"sort": [
{ "sales_count": { "order": "desc" } }
],
"from": 0,
"size": 20
}商品详情缓存
商品详情缓存采用 Redis Cache-Aside + Caffeine 本地缓存 两级缓存架构。
┌──────────────────────┐
│ 客户端请求 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 1. 查询 Caffeine │
│ 本地缓存(L1) │────── 命中 → 直接返回
└──────────┬───────────┘
│ 未命中
▼
┌──────────────────────┐
│ 2. 查询 Redis(L2) │────── 命中 → 回填 L1 后返回
└──────────┬───────────┘
│ 未命中
▼
┌──────────────────────┐
│ 3. 查询 MySQL │────── 查库 → 回填 L2 → 回填 L1 → 返回
└──────────────────────┘
│
▼
┌──────────────────────┐
│ 4. 返回响应 │
└──────────────────────┘import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class ProductCacheService {
private static final String PRODUCT_CACHE_KEY = "product:detail:";
private static final long LOCAL_MAX_SIZE = 10000;
private static final long REDIS_TTL_SECONDS = 3600;
private final RedisTemplate<String, String> redisTemplate;
private final Cache<Long, String> localCache;
public ProductCacheService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
this.localCache = Caffeine.newBuilder()
.maximumSize(LOCAL_MAX_SIZE)
.expireAfterWrite(60, TimeUnit.SECONDS)
.recordStats()
.build();
}
/**
* 获取商品详情,两级缓存策略
*/
public String getProductDetail(Long spuId) {
// 1. 查询本地缓存
String detail = localCache.getIfPresent(spuId);
if (detail != null) {
return detail;
}
// 2. 查询 Redis 缓存
String redisKey = PRODUCT_CACHE_KEY + spuId;
detail = redisTemplate.opsForValue().get(redisKey);
if (detail != null) {
// 回填本地缓存
localCache.put(spuId, detail);
return detail;
}
// 3. 查询数据库
detail = queryFromDatabase(spuId);
if (detail != null) {
// 回填 Redis 缓存(设置过期时间)
redisTemplate.opsForValue().set(redisKey, detail, REDIS_TTL_SECONDS, TimeUnit.SECONDS);
// 回填本地缓存
localCache.put(spuId, detail);
}
return detail;
}
/**
* 更新商品详情后清除缓存
*/
public void evictProductCache(Long spuId) {
localCache.invalidate(spuId);
redisTemplate.delete(PRODUCT_CACHE_KEY + spuId);
}
/**
* 模拟数据库查询(实际调用 DAO)
*/
private String queryFromDatabase(Long spuId) {
// 调用 DAO 查询 SPU 详情,序列化为 JSON
return null;
}
}商品上下架 / 审核流程(状态机)
商品从创建到上架经历一系列状态流转,使用状态模式或状态机来管理。
┌──────────┐
│ 草稿 │
└─────┬────┘
│ 提交审核
▼
┌──────────┐
│ 待审核 │◄────────────────────────┐
└─────┬────┘ │
┌───────┼────────┐ │
│ │ │ │
▼ ▼ ▼ │
┌────────┐ ┌──────┐ ┌────────┐ │
│ 审核通过 │ │审核 │ │ 审核 │ │
│ │ │退回 │ │ 撤销 │──────────────┘
└───┬────┘ └──────┘ └────────┘
│ 上架
▼
┌──────────┐ ┌──────────┐
│ 上架 │────────► │ 下架 │
│ (在售) │◄──────── │ (停售) │
└──────────┘ 上架 └──────────┘import java.util.EnumMap;
import java.util.Map;
/**
* 商品状态机
*/
public enum ProductStatus {
DRAFT(0, "草稿"),
PENDING_AUDIT(1, "待审核"),
AUDIT_PASSED(2, "审核通过"),
AUDIT_REJECTED(3, "审核驳回"),
ON_SHELF(4, "上架"),
OFF_SHELF(5, "下架");
private final int code;
private final String desc;
ProductStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
/**
* 状态流转定义表(当前状态 -> 允许到达的下一个状态)
*/
private static final Map<ProductStatus, ProductStatus[]> TRANSITIONS = new EnumMap<>(ProductStatus.class);
static {
TRANSITIONS.put(DRAFT, new ProductStatus[]{PENDING_AUDIT});
TRANSITIONS.put(PENDING_AUDIT, new ProductStatus[]{AUDIT_PASSED, AUDIT_REJECTED, DRAFT});
TRANSITIONS.put(AUDIT_PASSED, new ProductStatus[]{ON_SHELF, DRAFT});
TRANSITIONS.put(AUDIT_REJECTED, new ProductStatus[]{DRAFT});
TRANSITIONS.put(ON_SHELF, new ProductStatus[]{OFF_SHELF});
TRANSITIONS.put(OFF_SHELF, new ProductStatus[]{ON_SHELF, PENDING_AUDIT});
}
/**
* 校验状态是否允许流转
*
* @param current 当前状态
* @param target 目标状态
* @return 是否允许流转
*/
public static boolean canTransition(ProductStatus current, ProductStatus target) {
ProductStatus[] allowed = TRANSITIONS.get(current);
if (allowed == null) {
return false;
}
for (ProductStatus s : allowed) {
if (s == target) {
return true;
}
}
return false;
}
}@Service
public class ProductAuditService {
private final SpuRepository spuRepository;
public ProductAuditService(SpuRepository spuRepository) {
this.spuRepository = spuRepository;
}
/**
* 商品审核通过
*/
@Transactional
public void approve(Long spuId, String auditor) {
Spu spu = spuRepository.findById(spuId)
.orElseThrow(() -> new IllegalArgumentException("商品不存在: " + spuId));
ProductStatus current = ProductStatus.values()[spu.getStatus()];
ProductStatus target = ProductStatus.AUDIT_PASSED;
if (!ProductStatus.canTransition(current, target)) {
throw new IllegalStateException(
String.format("当前状态 %s 不允许流转到 %s", current.getDesc(), target.getDesc()));
}
spu.setStatus(target.getCode());
spu.setAuditStatus(2); // 审核通过
spu.setAuditRemark("审核通过,审核人: " + auditor);
spuRepository.update(spu);
}
/**
* 商品上架
*/
@Transactional
public void putOnShelf(Long spuId) {
Spu spu = spuRepository.findById(spuId)
.orElseThrow(() -> new IllegalArgumentException("商品不存在: " + spuId));
ProductStatus current = ProductStatus.values()[spu.getStatus()];
ProductStatus target = ProductStatus.ON_SHELF;
if (!ProductStatus.canTransition(current, target)) {
throw new IllegalStateException(
String.format("当前状态 %s 不允许上架", current.getDesc()));
}
// 检查是否有启用中的 SKU
List<Sku> skus = skuRepository.findBySpuId(spuId);
boolean hasActiveSku = skus.stream().anyMatch(s -> s.getStatus() == 1);
if (!hasActiveSku) {
throw new IllegalStateException("商品没有可售的 SKU,无法上架");
}
spu.setStatus(target.getCode());
spuRepository.update(spu);
}
}品牌管理
品牌表设计
CREATE TABLE `brand` (
`brand_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '品牌 ID',
`brand_name` VARCHAR(200) NOT NULL COMMENT '品牌名称',
`brand_name_en` VARCHAR(200) DEFAULT NULL COMMENT '品牌英文名称',
`logo_url` VARCHAR(500) DEFAULT NULL COMMENT '品牌 Logo',
`brand_story` TEXT COMMENT '品牌故事',
`country` VARCHAR(50) DEFAULT NULL COMMENT '原产国',
`website` VARCHAR(500) DEFAULT NULL COMMENT '品牌官网',
`brand_rank` TINYINT DEFAULT 0 COMMENT '品牌档次: 1-大众 2-中端 3-高端 4-奢侈',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-待审核 1-启用 2-禁用 3-审核驳回',
`audit_status` TINYINT NOT NULL DEFAULT 0 COMMENT '审核状态: 0-未提交 1-审核中 2-通过 3-驳回',
`auth_cert_urls` JSON COMMENT '品牌授权证书图片列表',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序值',
`deleted` TINYINT NOT NULL DEFAULT 0,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`brand_id`),
KEY `idx_status` (`status`),
KEY `idx_name` (`brand_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='品牌表';
-- 品牌与后台类目关联表(一个品牌可关联多个类目)
CREATE TABLE `brand_category` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`brand_id` BIGINT NOT NULL COMMENT '品牌 ID',
`category_id` BIGINT NOT NULL COMMENT '后台类目 ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_brand_cat` (`brand_id`, `category_id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='品牌-类目关联表';品牌关联类目 & 资质审核
品牌与类目之间的关系是多对多——一个品牌可以归属多个类目,一个类目下可以有多个品牌。例如"Apple"品牌同时关联"智能手机"和"笔记本电脑"两个类目。
品牌资质审核是电商平台的关键风控环节,主要流程如下:
商家提交品牌申请
│
▼
提交品牌资料(品牌名称 / Logo / 授权证书 / 商标注册证)
│
▼
平台审核员初审 → 资质真实性核验
│
├── 通过 → 品牌启用
│
└── 驳回 → 商家修改后重新提交@Service
public class BrandService {
private final BrandRepository brandRepository;
public BrandService(BrandRepository brandRepository) {
this.brandRepository = brandRepository;
}
/**
* 商家提交品牌审核
*/
@Transactional
public Long submitBrandAudit(BrandSubmitRequest request) {
Brand brand = new Brand();
brand.setBrandName(request.getBrandName());
brand.setLogoUrl(request.getLogoUrl());
brand.setBrandStory(request.getBrandStory());
brand.setStatus(0); // 待审核
brand.setAuditStatus(1); // 审核中
brand.setAuthCertUrls(request.getAuthCertUrls());
brandRepository.insert(brand);
// 关联类目
if (request.getCategoryIds() != null && !request.getCategoryIds().isEmpty()) {
brandRepository.insertBrandCategories(brand.getBrandId(), request.getCategoryIds());
}
return brand.getBrandId();
}
/**
* 审核品牌
*/
@Transactional
public void auditBrand(Long brandId, boolean approved, String remark) {
Brand brand = brandRepository.findById(brandId);
if (brand == null) {
throw new IllegalArgumentException("品牌不存在");
}
if (brand.getAuditStatus() != 1) {
throw new IllegalStateException("品牌不在审核中状态");
}
if (approved) {
brand.setStatus(1); // 启用
brand.setAuditStatus(2); // 审核通过
} else {
brand.setStatus(3); // 审核驳回
brand.setAuditStatus(3); // 驳回
}
brandRepository.update(brand);
}
}商品发布流程
完整发布流程
商品发布是一条从创建到售卖的完整链路,包含多个环节和状态切换。
草稿 ──→ 提交审核 ──→ 审核中 ──→ 审核通过 ──→ 上架
│ │ │
│ │ │
├── 随时编辑保存 │ │
│ (自动升级版本) │ │
│ │ │
│ ← 审核驳回 → 重新编辑 → 再次提交
│ │
├───────────────────────────────────────────┘
│ 直接取消发布
│
▼
下架(可再次上架或删除)多版本管理
商品编辑采用多版本管理策略:每次编辑生成新版本,审核通过后替换线上版本。
CREATE TABLE `spu_version` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`spu_id` BIGINT NOT NULL COMMENT 'SPU ID',
`version` INT NOT NULL COMMENT '版本号',
`snapshot_data` JSON NOT NULL COMMENT '版本快照(SPU + SKU 完整数据)',
`change_log` VARCHAR(500) DEFAULT NULL COMMENT '变更说明',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-编辑中 1-审核中 2-已发布 3-已废弃',
`operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_spu_version` (`spu_id`, `version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SPU 版本快照表';版本管理的关键逻辑:
@Service
public class ProductPublishService {
private final SpuRepository spuRepository;
private final SkuRepository skuRepository;
private final SpuVersionRepository versionRepository;
public ProductPublishService(SpuRepository spuRepository,
SkuRepository skuRepository,
SpuVersionRepository versionRepository) {
this.spuRepository = spuRepository;
this.skuRepository = skuRepository;
this.versionRepository = versionRepository;
}
/**
* 保存草稿(编辑保存,每次保存版本号 +1)
*/
@Transactional
public void saveDraft(Long spuId, SpuUpdateRequest request) {
Spu spu = spuRepository.findById(spuId);
if (spu == null) {
throw new IllegalArgumentException("商品不存在");
}
// 只有草稿和审核驳回状态允许编辑
if (spu.getStatus() != 0 && spu.getStatus() != 3) {
throw new IllegalStateException("当前状态不允许编辑");
}
// 更新 SPU 信息
spu.setName(request.getName());
spu.setSubtitle(request.getSubtitle());
spu.setCategoryId(request.getCategoryId());
spu.setBrandId(request.getBrandId());
spu.setMainImages(request.getMainImages());
spu.setDescription(request.getDescription());
spu.setVersion(spu.getVersion() + 1);
spuRepository.update(spu);
// 保存版本快照
SpuVersion version = new SpuVersion();
version.setSpuId(spuId);
version.setVersion(spu.getVersion());
version.setSnapshotData(buildSnapshot(spu, request.getSkus()));
version.setStatus(0); // 编辑中
version.setOperator(request.getOperator());
versionRepository.insert(version);
}
/**
* 提交审核
*/
@Transactional
public void submitForAudit(Long spuId) {
Spu spu = spuRepository.findById(spuId);
ProductStatus current = ProductStatus.values()[spu.getStatus()];
if (!ProductStatus.canTransition(current, ProductStatus.PENDING_AUDIT)) {
throw new IllegalStateException("当前状态不允许提交审核");
}
// 校验商品信息的完整性
validateProductCompleteness(spu);
spu.setStatus(ProductStatus.PENDING_AUDIT.getCode());
spu.setAuditStatus(1); // 审核中
spuRepository.update(spu);
// 将最新版本标记为审核中
SpuVersion latestVersion = versionRepository.findLatestBySpuId(spuId);
if (latestVersion != null) {
latestVersion.setStatus(1);
versionRepository.update(latestVersion);
}
}
/**
* 商品完整性校验
*/
private void validateProductCompleteness(Spu spu) {
if (spu.getName() == null || spu.getName().trim().isEmpty()) {
throw new IllegalArgumentException("商品名称不能为空");
}
if (spu.getCategoryId() == null) {
throw new IllegalArgumentException("请选择商品类目");
}
if (spu.getMainImages() == null || spu.getMainImages().isEmpty()) {
throw new IllegalArgumentException("请至少上传一张商品主图");
}
// 校验 SKU
List<Sku> skus = skuRepository.findBySpuId(spu.getSpuId());
if (skus == null || skus.isEmpty()) {
throw new IllegalArgumentException("请至少添加一个 SKU");
}
boolean hasValidPrice = skus.stream().anyMatch(s -> s.getPrice() != null && s.getPrice().compareTo(BigDecimal.ZERO) > 0);
if (!hasValidPrice) {
throw new IllegalArgumentException("请为 SKU 设置有效价格");
}
}
private String buildSnapshot(Spu spu, List<SkuRequest> skus) {
// 构建 SPU + SKU 的完整 JSON 快照
Map<String, Object> snapshot = new HashMap<>();
snapshot.put("spu", spu);
snapshot.put("skus", skus);
return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(snapshot);
}
}规格生成 SKU 算法
多规格商品在发布时,需要根据销售属性的笛卡尔积生成 SKU。以下为规格组合生成算法实现。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 规格生成 SKU 算法
*
* 给定多个销售属性及其值列表,计算所有可能的 SKU 规格组合。
* 支持排除不需要的组合(如某些颜色没有对应的尺码)。
*/
public class SkuSpecGenerator {
/**
* 生成 SKU 规格组合列表
*
* @param specList 销售属性列表,每个元素是 (attrId, attrName, values)
* @param exclude 需要排除的组合(可选),格式为 "attrId:value,attrId:value"
* @return 所有 SKU 组合
*/
public static List<SkuCombination> generate(List<SalesAttribute> specList,
List<Map<Long, String>> exclude) {
if (specList == null || specList.isEmpty()) {
return List.of();
}
// 1. 提取每个属性的值列表
List<List<SkuSpecValue>> attrValueList = specList.stream()
.map(attr -> attr.getValues().stream()
.map(val -> new SkuSpecValue(attr.getAttrId(), attr.getAttrName(), val))
.collect(Collectors.toList()))
.collect(Collectors.toList());
// 2. 计算笛卡尔积
List<List<SkuSpecValue>> cartesianProduct = cartesianProduct(attrValueList);
// 3. 过滤排除的组合
return cartesianProduct.stream()
.filter(combination -> !isExcluded(combination, exclude))
.map(combination -> {
SkuCombination sku = new SkuCombination();
sku.setSalesAttrs(combination.stream()
.map(v -> new SalesAttrEntry(v.getAttrId(), v.getAttrName(), v.getValue()))
.collect(Collectors.toList()));
// 自动生成 SKU 名称
sku.setSkuName(combination.stream()
.map(SkuSpecValue::getValue)
.collect(Collectors.joining(" ")));
return sku;
})
.collect(Collectors.toList());
}
/**
* 计算笛卡尔积
*/
private static <T> List<List<T>> cartesianProduct(List<List<T>> lists) {
List<List<T>> result = new ArrayList<>();
if (lists.isEmpty()) {
result.add(new ArrayList<>());
return result;
}
cartesianRecursive(lists, 0, new ArrayList<>(), result);
return result;
}
private static <T> void cartesianRecursive(List<List<T>> lists, int depth,
List<T> current, List<List<T>> result) {
if (depth == lists.size()) {
result.add(new ArrayList<>(current));
return;
}
for (T item : lists.get(depth)) {
current.add(item);
cartesianRecursive(lists, depth + 1, current, result);
current.remove(current.size() - 1);
}
}
/**
* 判断组合是否在排除列表中
*/
private static boolean isExcluded(List<SkuSpecValue> combination,
List<Map<Long, String>> exclude) {
if (exclude == null || exclude.isEmpty()) {
return false;
}
for (Map<Long, String> excludedItem : exclude) {
boolean matchAll = excludedItem.entrySet().stream()
.allMatch(entry -> combination.stream()
.anyMatch(spec ->
spec.getAttrId().equals(entry.getKey())
&& spec.getValue().equals(entry.getValue())));
if (matchAll) {
return true;
}
}
return false;
}
// ---- 内部数据类 ----
public static class SalesAttribute {
private Long attrId;
private String attrName;
private List<String> values;
// 构造函数、getter、setter
public SalesAttribute(Long attrId, String attrName, List<String> values) {
this.attrId = attrId;
this.attrName = attrName;
this.values = values;
}
public Long getAttrId() { return attrId; }
public String getAttrName() { return attrName; }
public List<String> getValues() { return values; }
}
public static class SkuSpecValue {
private Long attrId;
private String attrName;
private String value;
public SkuSpecValue(Long attrId, String attrName, String value) {
this.attrId = attrId;
this.attrName = attrName;
this.value = value;
}
public Long getAttrId() { return attrId; }
public String getAttrName() { return attrName; }
public String getValue() { return value; }
}
public static class SalesAttrEntry {
private Long attrId;
private String attrName;
private String value;
public SalesAttrEntry(Long attrId, String attrName, String value) {
this.attrId = attrId;
this.attrName = attrName;
this.value = value;
}
public Long getAttrId() { return attrId; }
public String getAttrName() { return attrName; }
public String getValue() { return value; }
}
public static class SkuCombination {
private String skuName;
private List<SalesAttrEntry> salesAttrs;
public String getSkuName() { return skuName; }
public void setSkuName(String skuName) { this.skuName = skuName; }
public List<SalesAttrEntry> getSalesAttrs() { return salesAttrs; }
public void setSalesAttrs(List<SalesAttrEntry> salesAttrs) { this.salesAttrs = salesAttrs; }
}
/**
* 使用示例
*/
public static void main(String[] args) {
// 定义销售属性
List<SalesAttribute> specs = new ArrayList<>();
specs.add(new SalesAttribute(1L, "颜色", List.of("黑色", "白色", "蓝色")));
specs.add(new SalesAttribute(2L, "内存", List.of("128GB", "256GB")));
specs.add(new SalesAttribute(3L, "网络", List.of("全网通", "移动版")));
// 定义排除组合:没有"白色-移动版"这个 SKU
List<Map<Long, String>> exclude = new ArrayList<>();
exclude.add(Map.of(1L, "白色", 3L, "移动版"));
// 生成 SKU
List<SkuCombination> result = generate(specs, exclude);
System.out.println("生成的 SKU 数量: " + result.size());
result.forEach(sku -> System.out.println(" SKU: " + sku.getSkuName()));
// 输出:
// 生成的 SKU 数量: 11
// SKU: 黑色 128GB 全网通
// SKU: 黑色 128GB 移动版
// SKU: 黑色 256GB 全网通
// SKU: 黑色 256GB 移动版
// SKU: 白色 128GB 全网通
// SKU: 白色 256GB 全网通
// SKU: 白色 256GB 移动版
// SKU: 蓝色 128GB 全网通
// SKU: 蓝色 128GB 移动版
// SKU: 蓝色 256GB 全网通
// SKU: 蓝色 256GB 移动版
// 注: 白色-移动版 被排除,3×2×2 - 1 = 11 个 SKU
}
}总结
电商架构的演进是一个不断解耦、不断抽象的过程。从单体架构到云原生,每一步演进都在解决前一阶段的痛点,同时引入新的复杂度。商品系统作为电商的核心域,其 SPU/SKU 模型设计、类目属性体系、数据库设计、搜索缓存策略、审核流程和品牌管理,共同构成了电商中台的基础设施。
在设计商品系统时,应重点关注以下原则:
- SPU/SKU 分离:抽象与具体分离,灵活支撑多规格商品
- 类目与属性解耦:后台管理类目绑定属性模板,前台展示类目灵活配置
- 缓存分层:本地缓存 + Redis 分布式缓存,平衡性能与一致性
- 状态机驱动:商品上下架、审核流程用状态机管理,避免状态混乱
- 版本可追溯:每次编辑保存版本快照,支持审核回滚和变更追溯