做电商后台这几年,我碰到最多的需求不是满减、不是优惠券,而是这个看起来平平无奇的“多规格属性”。刚上线的商品模块通常只有一个价格、一个库存,录个茶杯、笔记本完全够用。可一旦商家开始传服装、鞋帽、数码配件这类商品,问题就来了:一件T恤有白色、黑色、藏青三个颜色,又有S、M、L、XL四个尺码,每个颜色每个尺码的价格和库存还不一样,单价格字段怎么录?连带问题更多——下单时用户选了哪个颜色哪个尺码,后台怎么知道该扣哪一条库存?
这正是多规格属性要解决的问题。说白了,就是把一个商品的销售单元从“一个SPU对应一个SKU”升级成“一个SPU对应多个SKU”,其中每个SKU都代表一组规格组合,比如“白色-L”就是一个SKU。后台做多规格,不需要什么高深算法,但必须把数据模型、交互流程、异常边界这三件事理顺。这篇就把我在实际项目里搭建“商城后台多规格属性”的完整过程和踩过的坑捋一遍,给正在做类似功能的后端、前端、产品和独立开发者做个参考。
1. 需求拆解与整体设计思路
1.1 先搞清楚“多规格”到底在解决什么问题
很多刚接触电商系统的同学容易把“多规格属性”理解成“给商品多填几个字段”。真不是。它的本质是商品可配置化:商家在后台声明“我的商品有颜色和尺码两个维度”,系统自动把这些维度组合成多个可售卖的SKU,每个SKU拥有独立的条形码、价格、库存,并在下单、支付、发货时被准确定位。
举个例子。某商店卖一款帆布鞋,颜色有黑、白、红,尺码有36到40共5个。按单规格模型,就得录 3×5=15 个商品,用户在前台看到的是15个长得一样的商品卡片,买的时候还要自己比对什么颜色对应哪个商品,体验极差。用多规格模型,只需要录1个商品,后台自动生成15个SKU,前台一个商品详情页里用选择器切换颜色和尺码,看得到图片、价格、库存,选完加入购物车,闭着眼睛都知道自己买的是什么。
所以前期沟通需求时,第一件事就是跟业务方确认:这个“多规格”是要支持几个维度、允许多少选项、每个SKU是否独立价格库存。维度太少,后续扩展就麻烦;维度太多,维护成本直接爆炸。一般建议默认支持3到4个规格组,每个规格组最多支持50个规格值,这对绝大多数实物商品已经足够。
1.2 三种规格模式怎么选
做功能之前先理清规格的几种形态,这决定了后台的交互复杂度。
第一种是单规格模式,也就是传统的一个商品一个SKU,价格、库存都写在商品主表里。很多后台为了兼容旧数据,保留这种模式,新建商品时默认使用。
第二种是多规格模式,商家手动选择多个规格组和规格值,系统自动组合生成SKU列表。这是最常见的形态,适合服装、鞋包、数码配件等商品。
第三种是自义定规格模式,商家不选系统预置的规格组,而是自己输入“口味”“包装方式”等非标属性,系统再据此生成SKU。这种适合食品、生鲜、定制类目。
三种模式不是互斥的。我在实际项目里通常把它们整合在一个商品编辑页里:页面顶部有“是否启用多规格”的开关,开启后展示规格配置区,关闭则走单规格填写。这样老商家无感知,新商家又可以用高级功能,运维压力也小。
1.3 规格层级关系拆解
把规格拆成三层,整个设计就清晰了。
- 规格组:比如“颜色”“尺码”“套餐”,是属性的维度名,可以全局预置,也允许商家自定义。
- 规格值:挂在规格组下的具体选项,比如“颜色”下的“白色”“黑色”,一个规格组下可以有多个规格值。
- SKU:规格组的笛卡尔积组合,每个组合生成一个独立可售单元,承载价格、库存、编码等交易字段。
这个三层关系对数据结构设计和界面交互都有直接影响。首先,规格组和规格值最好独立维护,不要硬编码在商品表里,否则每加一个规格组就要改表结构。其次,界面上的录入流程也按这三层走:先选规格组,再勾选规格值,系统自动生成SKU矩阵,最后商家逐条维护单价库存。这样录入路径短、出错率低,商家也容易理解。
2. 数据模型设计与核心原理
2.1 表结构这么搭才灵活
数据模型是多规格属性的地基。我推荐采用SPU-SKU双层结构,并加独立的规格组和规格值表。
SPU表(商品表):存储商品公共信息,比如商品名、类目、状态、主图、描述等,一个SPU代表一个可展示的商品。
规格组表(spec_group):存储规格组与SPU的关联。字段包括:id、spu_id、规格组名称、排序值。一个SPU下可以有多个规格组。
规格值表(spec_value):存储规格值及归属关系。字段包括:id、spu_id、规格组id、规格值名称、排序值。规格值必须挂在某个规格组下。
SKU表:存储最终可售单元。核心字段包括:SKU_ID、SPU_ID、SKU编码、价格、库存、规格组合描述(如“黑色 / M”)、规格值ID串(如“12,45”)、状态。价格和库存必须冗余在这里,不能去规格值里取。
有些项目为了省事会把规格组和规格值直接以JSON结构塞进SPU表的detail字段里,再用程序解析。这种方案对简单场景确实可行,但后续查“某个规格值被哪些SPU引用”“某个SKU卖了多少件”会非常痛苦。建议还是规范成独立表,虽然建表多一些,但扩展性和查询效率都会好很多。
2.2 为什么要冗余规格信息到SKU表
这是个设计上的关键点,很多人容易忽略:SKU表除了存规格值ID串,还应该冗余一份规格组合描述字符串。
原因是,规格值有可能会被修改或删除。比如商家把原来的“深蓝色”改名为“藏青色”,如果SKU表只存规格值ID,那SQL联表查出来的显示名就是“藏青色”,订单记录也随之变化,听起来没问题,但历史订单已经打印、已经发货的怎么办?如果要审计“当时用户买的到底是什么颜色”,以关联表当前的值为准就有风险。
稳妥的做法是,在SKU表里同时存规格值ID和规格值名称快照。生成SKU时写入一份完整名称,比如“黑色 / L / 标准版”,后续即使名称改动了,历史SKU和订单也保持原来的展示文案。规格值ID则用于前台过滤和后台编辑时的勾选回显,两者分工明确。
2.3 规格值引用与变更的数据安全问题
再往深一层说,规格值不是孤立数据。一个预置规格值“红色”可能被几百个SPU引用,一旦改动名称,所有引用它的商品前台显示都会变。
所以规格值表建议设计成两级:公共规格字典和商品私有规格值。公共字典面向全站,比如电子类目的“颜色”里预置“黑色”“白色”;商品私有规格值允许商家新增“渐变紫”“雾霾蓝”这种非标项,关联到当前SPU,不影响全局。每次新建SPU时,商家先从公共字典里选规格组和值,再补充自定义值,系统把选中的值和自定义值合并后生成SKU。
修改规格值时还需要做引用检测。如果某个规格值已经被历史订单引用,那只能“新增一个值”而不能“改名覆盖”,否则订单数据就对不上了。这个逻辑虽然简单,但真能挡住不少后续的数据坑。
3. 后台功能搭建与实操流程
3.1 规格字典管理先做起来
动手开发前,第一步是把规格字典管理做扎实。这指的是维护全站可见的规格组和规格值,类似于基础数据。我当时的做法是做一个独立的“规格属性管理”菜单,里面按类目展示常用规格组,比如:
- 服装类目:颜色、尺码、袖长
- 数码类目:版本、颜色、套餐
- 食品类目:口味、包装规格
每个规格组下面维护规格值和排序。预置的规格组越多,商家录入时就越省事,但也不能无节制,否则后台列表里冗余项比有效项还多,反而干扰选择。我建议预置顺序按商品类目区分,类目选择了“服装”就优先展示“颜色”“尺码”,选择了“数码”就优先展示“版本”“颜色”,没有匹配类目的再展示全部。
字典管理模块的另一个职责是统一编码规则。每个规格值分配一个全局唯一的ID,不能因为商家A也建了一个“白色”就和商家B的“白色”冲突。公共字典的ID是系统生成的,自定义规格值走自增也可以,但要在名称上做去重校验,同规格组下不允许出现两个完全相同的规格值名称。
3.2 发布商品时自动生成SKU矩阵
核心流程在这里。商家的操作路径是:填写SPU基础信息 → 开启多规格开关 → 选择规格组 → 勾选或新增规格值 → 系统生成SKU列表 → 逐条维护价格库存 → 保存。
注意,SKU列表的生成是整个功能交互的枢纽。实现上就是笛卡尔积:先取规格组A的所有值,再取规格组B的所有值,递归组合出全部组合项。比如颜色有黑、白,尺码有S、M、L,那么组合就是 黑S、黑M、黑L、白S、白M、白L 共6个SKU。
前端生成笛卡尔积时,我一般用递归函数处理,几层都通用。后端保存时则需要重新计算一次,防止前端被篡改或漏传数据。核心校验逻辑包括:每个SKU的规格值组合不能重复、必须有价格、库存可以不填但要有默认值。价格和库存的录入方式也有讲究:自动生成的SKU列表默认继承SPU总价格和总库存,商家可以单独改某个SKU的数据,也可以选中多个SKU批量填充,这比逐条手填效率高得多。
下面是一个后端保存SKU时的示例逻辑(简化版Java风格):
public void createSku(SpuDto spu) { // 1. 删除当前SPU下的旧SKU skuMapper.deleteBySpuId(spu.getId()); // 2. 计算规格组合 List<List<SpecValueDto>> specList = groupSpecValues(spu.getSpecGroups()); List<List<SpecValueDto>> combinations = cartesianProduct(specList); // 3. 生成SKU记录 for (List<SpecValueDto> combination : combinations) { Sku sku = new Sku(); sku.setSpuId(spu.getId()); sku.setSpecDesc(buildSpecDesc(combination)); sku.setSpecValueIds(buildSpecValueIds(combination)); sku.setPrice(spu.getDefaultPrice()); sku.setStock(spu.getDefaultStock()); sku.setStatus(1); skuMapper.insert(sku); } }这个示例里有两个点可以再优化。一是“组合描述”的拼接规则要稳定,比如各规格组之间用“/”分隔,组内的名和值之间用空格分隔,保证生成结果一致。二是商品已生成订单后,删除旧SKU这种操作要小心,更稳妥的方式是逻辑删除或加“可售状态”标记,不能物理删掉。
3.3 多规格SKU编辑的几点交互细节
SKU矩阵生成之后,商家还要逐条维护价格、库存、条码、图片等信息。交互细节直接决定这个功能好不好用,这里有几个经验分享。
第一,表格分批渲染。如果一个SKU组合达到几十个,一次性渲染整个表格会让页面卡顿。建议分页或虚拟滚动,每页显示20条左右,同时提供“批量操作”按钮,比如批量设置库存、批量改价。
第二,规格图片的处理。很多商品是规格值不同、图片不同,比如每个颜色一张独立图。这里我推荐把图片挂在规格值维度上,而不是SKU维度。因为颜色值通常只有几个,而SKU可能是几十个,挂在SKU上意味着要给每个SKU都传图,商家会疯掉。前台展示时,按当前选中的规格值去匹配对应的规格值图片即可,如果找不到,再用SPU主图兜底。
第三,SKU编码的自动生成。如果一个SPU下有15个SKU,让商家手填编码肯定不现实。我建议根据SPU编号加序号自动生成,比如“SPU10001-001”、“SPU10001-002”,同时允许商家手动修改某个SKU的编码,但要做唯一性校验。编码一旦生成并引用到订单里,就不要再允许修改了,否则售后查单对不上号。
第四,保存时的全量替换策略。前端提交SKU时,我建议直接提交整个SKU列表,后端先删除旧的再插入新的,而不是逐条做diff。这样做逻辑最简单,还能避免“前端只改了价格,后端却不知道哪个SKU被删了”的脏数据问题。代价是更新频率高时会有点浪费IO,但对后台系统来说完全可接受。
3.4 编辑与回显的前后端配合
商品发布后,商家一定还会回来改价格、调库存甚至加规格值。编辑页的回显是另一个容易出错的地方。
回显要显示三块内容:当前SPU已选择的规格组、已选择的规格值、已生成的SKU列表及其价格库存。规格组和规格值的回显相对简单,直接用SKU表里的规格值ID串去重再关联名称即可。这里有个小坑:规格组和规格值的展示顺序必须和生成时一致,否则商家看到SKU顺序变了,就会怀疑系统是否保存成功。
如果商家在编辑时新增了规格值,则系统只影响新增的那几个SKU,已存在的SKU数据尽量保留;如果删除了某个规格值,则对应的SKU应该被标记为“下架”而不是直接删除,因为历史订单可能还在引用。这些都是编辑态下需要额外处理的边界。
前端回显交互的另一个细节是规格组排序。前台商品详情页展示规格选择器时,顺序一般按后台设置的顺序来,比如先选颜色再选尺码,而不是反过来。所以规格组的顺序字段一定要保留,并在SKU组合生成时也按照这个顺序拼接描述,这样回显和展示才完全对齐。
4. 常见问题与排查技巧实录
4.1 笛卡尔积导致的SKU数量爆炸
多规格功能上线后,最容易被吐槽的就是“SKU太多了怎么维护”。比如一件衬衫,颜色8个,尺码5个,袖长2种,规格组选满之后就是80个SKU。录入的时候一翻页就是好几屏,商家直接崩溃。
对付这个问题,除了限制每组规格值数量(比如50个),更有效的是前台给商家明确的预警:勾选完规格组和规格值后,立即计算出组合总数并展示在界面上,超过50个就提示“组合数较多,建议精简规格”并阻止提交。实际上大多数商品三到四个规格维度就够了,超过四个维度不仅后台维护难,用户在前台也容易选懵,销量反而不高。
如果实在需要维护大量SKU,我建议把库存维度从SKU里拆出来,用“独立批次库存表”关联到SKU,这样价格的维护仍然走SKU,库存可以单独批量导入。条码、重量这些字段也尽量做成“可空,缺省继承SPU”,减少录入负担。
4.2 修改规格值名称的历史数据问题
前面提到过,规格值名称冗余在SKU里。实际操作中最常见的场景是:商家想把“深蓝色”改成“宝石蓝”,如果直接改公共规格值,所有引用该值的SKU描述都会变,包括历史订单。如果历史订单要追溯到当时的购买描述,就乱了。
安全做法是不改名,新增值。代码里做变更检测:当前规格组下已存在同名值时,提示“该规格值已被商品引用,如需修改请新增规格值并重新关联”。如果规格值没有被任何SKU引用,那允许直接改名和删除,此时只影响当前SPU和已生成的SKU,风险可控。
4.3 SKU图片挂在哪个维度最合适
这个问题在服装类商城尤其常见。每件衣服都有多角度图片,如果让商家给每个SKU传一组图(黑M、黑L、白M、白L各传一套),几乎等于劝退。合理做法是图片关联到规格值层级,也就是“黑色”对应一套图,“白色”对应一套图。
页面展示逻辑是:用户选了“黑色”和“L”,前台从规格值图片里取“黑色”的图,如果没有匹配,就回退到SPU主图。后台编辑时,商家在“颜色”规格值旁添加图片即可,不用管S还是M的尺码差异。如果确实存在某SKU的图特殊,比如一件衣服黑L是加长版,可以再提供一个“SKU图片覆盖”功能,但默认不启用,避免交互复杂度上升。
4.4 常见报错与解决方案速查表
把我在实际开发中遇到的高频问题整理成一张表,方便排查:
| 问题表现 | 可能原因 | 处理方案 |
|---|---|---|
| 保存后SKU数量比预期少 | 规格值名相同触发了去重校验 | 检查同规格组是否录入重复值,合并后再保存 |
| 前台价格展示全是默认价而非SKU独立价 | 前端SKU列表提交时没有携带price字段 | 检查对象字段映射是否丢失,后端增加非空校验 |
| SKU描述顺序时对时错 | 规格组排序不稳定 | 后端生成组合前按sort_order排序,固定拼接规则 |
| 历史订单里规格名变了 | SKU只存了规格值ID而没存名称快照 | 补做名称快照冗余,新数据写入快照 |
| 编辑保存后自定义规格值丢失 | 前端只提交了勾选值,没有提交新增值 | 前端在提交前将新增值合并到最终列表 |
| 删除规格值报外键错误 | 规格值被SKU或订单引用 | 改为逻辑删除,被引用时禁用删除按钮 |
| SKU编码重复 | 并发提交导致序号生成冲突 | 在数据库加唯一索引,并按SPU_ID前缀批量生成序号 |
| 商品详情页规格选择器无响应 | 前端规格值ID没有映射到SKU数据 | 检查SKU里保存的规格值ID串是否按顺序解析 |
4.5 性能与扩展性的提前规划
多规格本身不是重逻辑,但当SKU数量上来后,接口性能会暴露问题。商品详情页一次要返回SKU列表、规格值图片、库存、价格,如果每次都实时联表查询,QPS一高就扛不住。建议的做法是:
- SPU和SKU信息做一级缓存,按SPU_ID为维度设置合理的过期时间,比如5分钟。库存的实时性要求高,可以走单独的库存接口或Redis计数,而不是每次把整个SKU列表返回。
- SKU列表接口和订单创建接口要分开。详情页只需要SKU的基本信息和价格,SKU的库存可以延迟加载。
- 商品列表页不要返回全部SKU信息,只返回“最低价”和“总库存”作为列表展示,再点进详情页拿全量数据。这样列表接口的压力会小很多。
另外,如果以后要做预售、秒杀、多仓库存,SKU表里“库存”字段的单一设计就不够了。建议从第一天起就把库存抽象成“库存策略”,可以是SKU独立库存,也可以是仓库维度库存,后续扩展时再根据策略决定取值来源。
5. 落地复盘与个人体会
多规格属性这个功能,技术上不难,难的是需求边界和交互细节。
我在实际项目里最大的体会是:别贪多。一开始产品经理规划了四五个规格组、批量导入、价格阶梯、SKU封面图等等,看着很完整,可真落地上线,商家真正高频使用的只有“颜色+尺码”“版本+容量”这类两到三组规格,价格和库存的批量填充使用率也不高。倒不如先把基础链路做扎实:规格字典、笛卡尔积生成SKU、价格库存维护、订单引用校验,这四个环节稳定了,再往上面加花样。
另外有个容易忽略的点是权限和审核。商城后台通常有“商家后台”和“运营后台”两层。运营后台要能看到所有商家的SPU和SKU,并且能按规格值筛商品,比如查“所有白色带L码的T恤”。这要求SKU表里规格值ID串的存储格式必须统一,方便LIKE查询或单独拆一张规格关联表。如果只是临时用,JSON字段也能接受;但真要长期运营,建议建一张“SPU_SKU_SPEC”关联表,每个SKU每行存一个规格值ID,查询和统计都会顺手很多。
最后一条经验是关于测试数据的。多规格的核心流程是“编辑→保存→回显→下单→发货”,测试时一定要覆盖“改规格”这个场景:商家发布商品后,把某个尺码删掉、把某个颜色价格涨了、把一个规格值改名了,这时候已生成的SKU、购物车里还没下单的旧组合、历史订单的展示,都应该各自正常。这几个场景最容易出数据错乱,也是上线前最值得花时间回归测试的地方。
如果你正在做商城后台,建议参考上面的数据模型和流程先搭一版最小可用系统:SPU + 规格字典 + SKU生成 + 价格库存编辑,然后找真实商家试用,反馈哪个环节操作不顺再迭代。多规格这个功能做得好,直接决定商城能否承接服装、鞋包这类主力招商品类,值得多花心思打磨。