连锁餐饮的点餐体验里,套餐与加料是提升客单价和运营灵活性的核心功能。
LikeShop 连锁点餐系统原生已具备成熟的单品管理、订单流转、后厨打印、门店管理能力,但在实际业务落地中,快餐、奶茶、汉堡类商家普遍有两类强需求:一是组合套餐(主食+小食+饮料打包售卖,比单点更优惠),二是商品加料(糖度冰度、额外配料等自定义选项)。如果简单把套餐当成单品上架,会出现库存无法联动、成本核算不准、后厨看不到加料明细等问题。
这类需求属于典型的业务扩展型二开,最佳实践是复用原有商品与订单底座,新增扩展表承载套餐与加料逻辑,通过事件联动库存与后厨打印,不推翻原生核心流程,以最小侵入成本实现完整能力。
本文基于 LikeShop 连锁点餐 Java 版技术栈,从设计思路、表结构改造、核心代码实现、上下游联动四个维度,给出完整的二开落地方案。
一、改造背景与核心目标
1.1 原生能力的局限
原生系统以单品 SKU 为核心单位,能够满足基础点餐需求,但面对组合类运营场景存在明显短板:
- 套餐只能模拟单品:商家把套餐当作一个新商品上架,无法关联子商品库存,卖完了只能手动改库存,也没法核算各单品成本;
- 加料能力缺失:奶茶的糖度冰度、汉堡的加菜加酱这类自定义选项,只能在备注里填写,后厨容易漏看,也没法自动加价;
- 价格规则死板:固定套餐价、自选组合价、加料加价等灵活定价方式无法实现;
- 后厨打印颗粒度粗:套餐不展开子商品、备注里的加料不突出,后厨配餐效率低、错单率高。
1.2 二开核心目标
- 双类型套餐支持:覆盖固定套餐(固定搭配)与自选套餐(分组选品)两种主流模式;
- 分组加料能力:支持按组管理加料选项,单选/多选、加价/不加价可配置,部分加料可联动库存;
- 全链路联动:下单自动扣减对应子商品与配料库存,后厨打印自动展开套餐明细与加料备注;
- 最小侵入原则:新增扩展表承载业务,复用原生订单、库存、打印核心能力,不破坏原有主流程;
- 向下兼容:历史订单、纯单品点餐逻辑不受影响,可平滑升级。
二、核心设计思路
2.1 套餐组合模型
套餐本质是「特殊商品 + 子商品关联」的组合结构,本身作为一个 SPU 展示和下单,库存与成本按子商品核算。分为两类:
| 套餐类型 | 规则说明 | 典型场景 |
|---|---|---|
| 固定套餐 | 子商品固定不可选,打包成一个整体售卖 | 商务套餐、全家桶、指定组合餐 |
| 自选套餐 | 子商品按分组配置,每组可选 N 个 | 主食选1、饮料选1、小食选1的自由组合 |
核心规则:
- 套餐有独立售价,通常低于子商品单价总和,形成优惠感知;
- 库存扣减穿透到子商品 SKU,套餐本身不设独立库存;
- 退款时按子商品维度回补库存,支持整单退与部分退。
2.2 商品加料模型
加料挂载在单品上,按分组管理,支持灵活配置:
- 分组维度:如“糖度”“冰度”“配料”,每组可设置单选/多选、最少/最多可选数量;
- 选项类型:分为规格类(如少冰/去冰,不加价)和加料类(如加珍珠/加芝士,额外加价);
- 库存联动:实体配料类加料(如加鸡排、加蛋)可关联对应 SKU,下单自动扣库存。
2.3 与原生体系的关系
- 复用原生商品表、SKU 表、订单表、库存服务、后厨打印服务;
- 套餐、加料的扩展数据通过关联表挂载,不修改原生核心字段语义;
- 订单状态流转、支付、结算完全沿用原生逻辑,仅在下单和打印节点注入扩展数据。
三、数据库设计
全部采用「新增表为主、扩展字段为辅」的策略,尽量不改动原生表结构,降低后续官方版本升级的冲突成本。
3.1 套餐主表 ls_combo_set
套餐基础信息,本身关联一个商品SPU作为前台展示载体。
CREATE TABLE ls_combo_set (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
combo_name VARCHAR(64) NOT NULL COMMENT '套餐名称',
goods_id BIGINT NOT NULL COMMENT '关联商品ID(套餐作为SPU展示)',
combo_type TINYINT NOT NULL COMMENT '套餐类型:1固定套餐 2自选套餐',
original_price DECIMAL(10,2) NOT NULL COMMENT '子商品原价总和',
combo_price DECIMAL(10,2) NOT NULL COMMENT '套餐售价',
description VARCHAR(255) COMMENT '套餐说明',
sort INT DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1 COMMENT '0关闭 1启用',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_goods_id(goods_id),
INDEX idx_status(status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐主表';
3.2 套餐商品关联表 ls_combo_goods
套餐与子商品的关联关系,自选套餐通过 group_id 分组。
CREATE TABLE ls_combo_goods (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
combo_id BIGINT NOT NULL COMMENT '套餐ID',
group_id BIGINT DEFAULT 0 COMMENT '分组ID,固定套餐为0',
group_name VARCHAR(32) COMMENT '分组名称,如“主食”“饮料”',
goods_id BIGINT NOT NULL COMMENT '子商品ID',
sku_id BIGINT DEFAULT NULL COMMENT '子商品SKU_ID',
quantity INT NOT NULL DEFAULT 1 COMMENT '数量',
is_required TINYINT DEFAULT 1 COMMENT '是否必选:0否 1是',
sort INT DEFAULT 0,
INDEX idx_combo_id(combo_id),
INDEX idx_group_id(group_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐商品关联表';
3.3 加料分组表 ls_goods_extra_group
单个商品下的加料分组,一组对应一类选项。
CREATE TABLE ls_goods_extra_group (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
goods_id BIGINT NOT NULL,
group_name VARCHAR(32) NOT NULL COMMENT '分组名称:糖度/配料等',
select_type TINYINT NOT NULL COMMENT '选择类型:1单选 2多选',
min_select INT DEFAULT 0 COMMENT '最少可选数量',
max_select INT DEFAULT 1 COMMENT '最多可选数量',
sort INT DEFAULT 0,
status TINYINT DEFAULT 1,
INDEX idx_goods_id(goods_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品加料分组表';
3.4 加料选项表 ls_goods_extra_item
每组下的具体选项,支持加价与库存关联。
CREATE TABLE ls_goods_extra_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
group_id BIGINT NOT NULL,
item_name VARCHAR(32) NOT NULL COMMENT '选项名称',
extra_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '加价金额',
stock_linked TINYINT DEFAULT 0 COMMENT '是否关联库存:0否 1是',
sku_id BIGINT DEFAULT NULL COMMENT '关联配料SKU_ID',
sort INT DEFAULT 0,
status TINYINT DEFAULT 1,
INDEX idx_group_id(group_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='加料选项表';
3.5 订单商品表扩展
原生订单商品表增加两个字段,标记套餐层级关系,不破坏原有字段语义。
ALTER TABLE ls_order_goods
ADD COLUMN parent_id BIGINT DEFAULT 0 COMMENT '父商品ID,套餐子项对应主套餐ID',
ADD COLUMN is_combo TINYINT DEFAULT 0 COMMENT '是否套餐主商品:0否 1是';
3.6 订单加料明细表 ls_order_goods_extra
记录订单中每个商品对应的加料选项,作为后厨打印与结算依据。
CREATE TABLE ls_order_goods_extra (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_goods_id BIGINT NOT NULL COMMENT '订单商品明细ID',
extra_item_id BIGINT NOT NULL,
item_name VARCHAR(32) NOT NULL COMMENT '加料名称快照',
extra_price DECIMAL(10,2) NOT NULL COMMENT '当时加价金额',
quantity INT NOT NULL DEFAULT 1,
INDEX idx_order_goods_id(order_goods_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单加料明细表';
四、核心业务代码实现
4.1 套餐详情组装
前台点餐页展示套餐时,组装套餐结构与可选子商品,供用户选择。
@Service
public class ComboService {
@Autowired
private ComboSetMapper comboMapper;
@Autowired
private ComboGoodsMapper comboGoodsMapper;
@Autowired
private GoodsService goodsService;
public ComboDetailVO getComboDetail(Long comboId) {
ComboSet combo = comboMapper.selectById(comboId);
if (combo == null || combo.getStatus() != 1) {
throw new BizException("套餐不存在或已下架");
}
ComboDetailVO vo = BeanCopyUtils.copy(combo, ComboDetailVO.class);
// 固定套餐:直接返回子商品列表
if (combo.getComboType() == 1) {
List<ComboGoods> goodsList = comboGoodsMapper.listByComboId(comboId);
vo.setGoodsList(buildGoodsVO(goodsList));
return vo;
}
// 自选套餐:按分组返回
List<ComboGroupVO> groups = comboGoodsMapper.listByComboId(comboId).stream()
.collect(Collectors.groupingBy(ComboGoods::getGroupId))
.entrySet().stream()
.map(entry -> {
ComboGroupVO group = new ComboGroupVO();
group.setGroupId(entry.getKey());
group.setGroupName(entry.getValue().get(0).getGroupName());
group.setGoodsList(buildGoodsVO(entry.getValue()));
return group;
}).collect(Collectors.toList());
vo.setGroups(groups);
return vo;
}
private List<GoodsSimpleVO> buildGoodsVO(List<ComboGoods> list) {
return list.stream().map(item -> {
GoodsSimpleVO vo = new GoodsSimpleVO();
vo.setGoodsId(item.getGoodsId());
vo.setSkuId(item.getSkuId());
vo.setGoodsName(goodsService.getNameById(item.getGoodsId()));
vo.setQuantity(item.getQuantity());
return vo;
}).collect(Collectors.toList());
}
}
4.2 加料选项查询
进入商品详情时,加载该商品的所有加料分组与选项。
public List<ExtraGroupVO> getGoodsExtras(Long goodsId) {
List<GoodsExtraGroup> groups = extraGroupMapper.listByGoodsId(goodsId);
return groups.stream().map(group -> {
ExtraGroupVO vo = new ExtraGroupVO();
vo.setGroupId(group.getId());
vo.setGroupName(group.getGroupName());
vo.setSelectType(group.getSelectType());
vo.setMinSelect(group.getMinSelect());
vo.setMaxSelect(group.getMaxSelect());
List<ExtraItemVO> items = extraItemMapper.listByGroupId(group.getId()).stream()
.map(item -> BeanCopyUtils.copy(item, ExtraItemVO.class))
.collect(Collectors.toList());
vo.setItems(items);
return vo;
}).collect(Collectors.toList());
}
4.3 下单金额计算
核心计算逻辑:套餐优惠 + 加料加价,汇总到订单商品行。
@Service
public class OrderCalcService {
/**
* 计算单条商品的最终金额(含套餐与加料)
*/
public BigDecimal calcGoodsAmount(OrderGoodsCalcDTO goodsDTO) {
BigDecimal basePrice = goodsDTO.getUnitPrice();
// 1. 如果是套餐,使用套餐价(已包含子商品优惠)
if (goodsDTO.getIsCombo() == 1) {
basePrice = goodsDTO.getComboPrice();
}
// 2. 累加加料加价
if (CollectionUtils.isNotEmpty(goodsDTO.getExtraItems())) {
BigDecimal extraTotal = goodsDTO.getExtraItems().stream()
.map(ExtraItemDTO::getExtraPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
basePrice = basePrice.add(extraTotal);
}
// 3. 乘以数量
return basePrice.multiply(new BigDecimal(goodsDTO.getQuantity()))
.setScale(2, RoundingMode.HALF_UP);
}
}
4.4 订单落库与库存扣减
下单时写入套餐子商品、加料明细,并穿透扣减对应库存。
@Service
public class OrderCreateService {
@Transactional(rollbackFor = Exception.class)
public Long createOrder(OrderCreateDTO dto, Long userId) {
// 1. 创建订单主单(复用原生逻辑)
Long orderId = createBaseOrder(dto, userId);
for (OrderGoodsCreateDTO goodsDTO : dto.getGoodsList()) {
// 2. 写入主商品行
Long orderGoodsId = insertOrderGoods(orderId, goodsDTO);
// 3. 如果是套餐,写入子商品行
if (goodsDTO.getIsCombo() == 1) {
for (OrderGoodsCreateDTO subGoods : goodsDTO.getSubGoodsList()) {
insertSubOrderGoods(orderId, orderGoodsId, subGoods);
// 扣减子商品库存
stockService.deduct(subGoods.getSkuId(), subGoods.getQuantity());
}
} else {
// 单品扣库存
stockService.deduct(goodsDTO.getSkuId(), goodsDTO.getQuantity());
}
// 4. 写入加料明细
if (CollectionUtils.isNotEmpty(goodsDTO.getExtraItems())) {
batchInsertExtras(orderGoodsId, goodsDTO.getExtraItems());
// 关联库存的配料,扣减对应SKU库存
deductExtraStock(goodsDTO.getExtraItems(), goodsDTO.getQuantity());
}
}
// 5. 触发后厨打印事件
applicationEventPublisher.publishEvent(new OrderCreatedEvent(orderId));
return orderId;
}
}
4.5 后厨打印适配
后厨打印模板扩展套餐与加料的展示逻辑,确保后厨能清晰看到完整配餐信息。
// 打印内容组装
private String buildKitchenPrintContent(Order order) {
StringBuilder sb = new StringBuilder();
sb.append("=== 订单 ").append(order.getOrderSn()).append(" ===\n");
for (OrderGoods goods : order.getGoodsList()) {
if (goods.getIsCombo() == 1) {
// 套餐主商品
sb.append("【套餐】").append(goods.getGoodsName())
.append(" x").append(goods.getQuantity()).append("\n");
// 展开子商品
List<OrderGoods> subs = getSubGoods(goods.getId());
for (OrderGoods sub : subs) {
sb.append(" └ ").append(sub.getGoodsName())
.append(" x").append(sub.getQuantity()).append("\n");
}
} else {
sb.append(goods.getGoodsName())
.append(" x").append(goods.getQuantity()).append("\n");
}
// 追加加料明细
List<OrderGoodsExtra> extras = getExtras(goods.getId());
if (!extras.isEmpty()) {
for (OrderGoodsExtra extra : extras) {
sb.append(" + ").append(extra.getItemName()).append("\n");
}
}
}
return sb.toString();
}
五、上下游模块联动
套餐与加料不是孤立功能,必须联动库存、售后、统计等模块,才能保证全链路数据一致。
5.1 库存模块
- 套餐订单不扣套餐本身库存,穿透扣减每个子商品的 SKU 库存;
- 关联库存的加料选项,下单同步扣减配料 SKU 库存;
- 退款/退菜时,按子商品和配料维度分别回补库存,避免只退主商品、漏退子项。
5.2 售后模块
- 支持整单退、套餐内退单个子商品、退单个加料选项;
- 退款金额按子商品和加料的实际占比分摊计算;
- 退菜事件触发后厨打印退单提醒,避免后厨继续制作。
5.3 统计报表
- 套餐销量统计:区分各套餐的售卖份数、转化效果;
- 加料销量统计:热门加料排行,辅助商家优化配料采购;
- 营收统计:套餐收入、加料收入分别核算,支持按门店、按日期维度统计。
5.4 门店与权限
- 套餐与加料配置支持门店维度差异化,不同门店可启用不同套餐;
- 商家端可自主配置套餐与加料规则,平台端可审核与管控;
- 所有配置变更留操作日志,可追溯。
六、平滑升级与兼容方案
6.1 升级步骤
- 执行新增表 SQL 与订单商品表扩展字段 SQL,全部为增量操作,不修改原生核心数据;
- 部署套餐与加料模块代码,原生下单、支付、打印逻辑保持不变;
- 后台先开放给试点门店使用,验证库存、打印、结算全链路正确;
- 全量上线后同步更新商家端配置指引。
6.2 历史数据兼容
- 历史订单无套餐与加料数据,默认按纯单品处理,不影响原有数据;
- 报表统计支持「含套餐/不含套餐」两种口径,保证数据连续性;
- 原生纯商品点餐流程完全保留,商家可按需选择是否启用套餐功能。
6.3 官方版本升级兼容
- 核心改造均为新增表与新增服务,仅对订单商品表做少量字段扩展;
- 官方版本迭代时,重点核对订单商品表、库存接口、打印事件是否有变动,同步适配即可;
- 扩展逻辑与原生主流程解耦,升级冲突成本极低。
七、常见坑与避坑建议
1. 套餐库存不同步
坑:只处理套餐商品行,忘记扣减子商品库存,导致超卖。
避坑:库存扣减统一落到 SKU 维度,套餐下单必须遍历子商品逐个扣减,放在同一事务内,失败全部回滚。
2. 自选套餐校验不严
坑:前端限制可选数量,后端没校验,用户绕过前端提交异常数量。
避坑:下单接口二次校验每组的可选数量范围,必选组不能为空,超出最大数量直接拦截。
3. 后厨打印丢加料
坑:加料只存在订单明细里,打印模板没适配,后厨看不到加料,频繁做错餐。
避坑:打印模板强制展示加料明细,用缩进或特殊标记突出显示;退菜、改加料也要同步打印提醒。
4. 退款库存回补不全
坑:退款只回补主商品库存,子商品和配料库存不回补,越卖库存越少。
避坑:退款时递归处理套餐子项与加料,关联库存的全部回补;退款单和下单单走对称的库存处理逻辑。
5. 套餐价格分摊混乱
坑:套餐优惠只算在主商品上,子商品还是原价,财务对账对不上。
避坑:按子商品原价占比分摊套餐优惠额,保证各子商品的实付金额之和等于套餐价,结算和对账口径统一。
结语
连锁点餐的套餐与加料,看起来是前端展示的小功能,本质是商品、库存、订单、打印全链路的联动改造。
LikeShop 连锁点餐系统原生已经有非常扎实的订单与门店底座,二开时不用推翻重构,沿着「新增扩展表 + 复用核心能力 + 事件联动上下游」的思路,就能以很低的侵入成本实现完整的套餐与加料功能,同时保留后续官方版本的平滑升级能力。
对餐饮类二开项目来说,核心原则永远是:业务增量扩展,底座尽量不动。底层链路稳了,上面加再多运营玩法,都不会乱。