多商户平台的营销活动,和单商户的复杂度不在一个量级。
LikeShop 多商户高级版原生已经具备基础的营销能力,但在实际运营中,平台方往往需要更灵活的满减满赠体系:区分平台级与商家级活动、支持多档阶梯规则、控制优惠叠加范围、跨店满减自动分摊到商家、退款时按比例回退优惠。如果直接在原有订单逻辑里硬加判断,很容易导致计算混乱、分账不准、退款不同步等问题。
这类需求属于典型的营销扩展型二开,最佳实践是独立活动体系 + 统一计算入口 + 事件联动订单分账,不侵入原生订单主流程,以最小侵入成本实现可配置的满减满赠规则。
本文基于 LikeShop 多商户高级版(Java 技术栈),从架构设计、表结构、核心计算逻辑、上下游联动、平滑升级五个维度,给出一套完整的二开落地方案。
一、二开背景与核心目标
1.1 原生营销能力的局限
原生版本的营销工具以单店铺维度为主,在多商户平台级运营场景下存在几个明显短板:
- 层级单一:缺少「平台级活动」与「商家级活动」的分层,平台无法统一组织跨店大促;
- 规则简单:多为固定金额优惠,缺少多档阶梯满减、满折、满赠的组合玩法;
- 叠加模糊:活动之间、活动与优惠券之间能否叠加,缺少精细化的开关控制;
- 分摊缺失:跨店优惠无法自动按比例分摊到各商家子订单,结算和退款时账务对不上;
- 审核缺失:商家创建活动缺少平台审核环节,违规活动容易引发合规风险。
1.2 二开核心目标
- 双层级活动:支持平台级跨店满减、商家级店铺满减两套体系,各自独立管理;
- 多规则类型:覆盖满金额减钱、满金额打折、满金额赠商品三种主流玩法,支持多档阶梯;
- 叠加可控:可配置是否与优惠券、其他活动叠加,避免优惠叠加失控;
- 自动分摊:跨店优惠按子订单金额比例自动分摊,退款时按分摊额同步回退;
- 流程合规:商家活动走平台审核,状态机可控,全程留痕可追溯;
- 最小侵入:独立活动表 + 统一计算服务,尽量不修改原生订单与分账核心代码。
二、核心设计思路
2.1 活动双层级模型
| 层级 | 发起方 | 适用范围 | 优惠承担方 | 典型场景 |
|---|---|---|---|---|
| 平台级活动 | 平台运营 | 全平台或指定类目/商家 | 平台承担(从佣金中抵扣) | 618跨店满减、平台大促 |
| 商家级活动 | 入驻商家 | 本店商品 | 商家自行承担 | 店铺满减、店庆活动 |
2.2 三种规则类型
统一抽象为「满足条件 → 享受优惠」的阶梯模型,三种类型共用一套状态机:
- 满减:满 X 元减 Y 元,支持多档(满100减10、满200减30);
- 满折:满 X 元打 Y 折,支持多档;
- 满赠:满 X 元赠送指定商品,支持多档不同赠品。
2.3 优惠叠加规则
通过配置位精细控制叠加范围,避免优惠失控:
- 平台活动与商家活动:可配置是否可叠加;
- 活动与优惠券:可配置是否可叠加;
- 同类型活动:默认互斥,单笔订单取最优一档;
- 特殊商品:可排除指定商品不参与活动。
2.4 分摊原则
跨店满减的核心难点是账务分摊,遵循两条原则:
- 按金额比例分摊:优惠总额按各商家子订单实付占比拆分;
- 误差放最后:分摊四舍五入产生的几分钱误差,放在最后一个子订单消化,保证总额一致。
2.5 活动状态机
和其他营销活动共用一套状态流转,保证平台管控能力:
草稿 → 提交审核 → 审核通过 → 未开始 → 进行中 → 已结束
↓
审核驳回 → 修改重提
三、数据库设计
采用「主表存公共属性 + 规则表存阶梯明细 + 订单关联表存分摊快照」的三层结构,全部为新增表,极少修改原生表。
3.1 活动主表 ls_promotion_full
存储活动基础信息,平台级和商家级都在这一张表,通过 level 字段区分。
CREATE TABLE ls_promotion_full (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
promotion_name VARCHAR(64) NOT NULL COMMENT '活动名称',
promotion_type TINYINT NOT NULL COMMENT '类型:1满减 2满折 3满赠',
promotion_level TINYINT NOT NULL COMMENT '层级:1平台级 2商家级',
merchant_id BIGINT DEFAULT NULL COMMENT '商家ID,平台级为空',
scope_type TINYINT NOT NULL COMMENT '适用范围:1全部商品 2指定分类 3指定商品',
scope_ids TEXT COMMENT '范围ID集合,逗号分隔',
stackable_coupon TINYINT NOT NULL DEFAULT 0 COMMENT '是否可叠加优惠券:0否 1是',
stackable_other TINYINT NOT NULL DEFAULT 0 COMMENT '是否可叠加其他活动:0否 1是',
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
audit_status TINYINT NOT NULL DEFAULT 0 COMMENT '审核状态:0草稿 1待审 2通过 3驳回',
status TINYINT NOT NULL DEFAULT 0 COMMENT '活动状态:0未开始 1进行中 2已结束 3关闭',
create_by BIGINT,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_merchant_id(merchant_id),
INDEX idx_status(status),
INDEX idx_time(start_time, end_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='满减满赠活动主表';
3.2 活动阶梯规则表 ls_promotion_full_rule
一个活动对应多档阶梯规则,支持逐级递增。
CREATE TABLE ls_promotion_full_rule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
promotion_id BIGINT NOT NULL,
threshold_amount DECIMAL(10,2) NOT NULL COMMENT '满足门槛金额',
discount_type TINYINT NOT NULL COMMENT '优惠类型:1减金额 2折扣 3赠商品',
discount_value DECIMAL(10,2) NOT NULL COMMENT '优惠值:减钱/折扣率/赠品数量',
gift_sku_id BIGINT DEFAULT NULL COMMENT '赠品SKU_ID,满赠类型使用',
sort INT NOT NULL DEFAULT 0 COMMENT '档位排序,金额从低到高',
create_time DATETIME NOT NULL,
INDEX idx_promotion_id(promotion_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动阶梯规则表';
3.3 订单优惠关联表 ls_order_promotion_full
订单维度的优惠快照,记录使用了哪个活动、分摊了多少金额,是对账和退款回退的依据。
CREATE TABLE ls_order_promotion_full (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL COMMENT '子订单ID',
main_order_id BIGINT DEFAULT NULL COMMENT '主订单ID,跨店场景使用',
promotion_id BIGINT NOT NULL,
promotion_level TINYINT NOT NULL,
promotion_type TINYINT NOT NULL,
order_amount DECIMAL(10,2) NOT NULL COMMENT '子订单原金额',
discount_amount DECIMAL(10,2) NOT NULL COMMENT '本子单分摊优惠金额',
bearer_type TINYINT NOT NULL COMMENT '优惠承担方:1平台 2商家',
create_time DATETIME NOT NULL,
INDEX idx_order_id(order_id),
INDEX idx_main_order_id(main_order_id),
INDEX idx_promotion_id(promotion_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单活动优惠关联表';
3.4 原生表扩展
仅在订单表增加一个优惠汇总字段,不破坏原有结构:
ALTER TABLE ls_order
ADD COLUMN promotion_discount DECIMAL(10,2) DEFAULT 0.00 COMMENT '活动优惠总金额'
AFTER discount_amount;
四、核心业务逻辑与代码实现
4.1 活动创建与审核
商家在后台创建活动,选择类型、设置阶梯、指定范围,提交后进入平台审核。
@Service
public class PromotionFullService {
@Autowired
private PromotionFullMapper promotionMapper;
@Autowired
private PromotionFullRuleMapper ruleMapper;
@Transactional(rollbackFor = Exception.class)
public Long create(PromotionFullCreateDTO dto, Long merchantId) {
// 1. 校验规则合法性:阶梯金额递增、优惠值合理
validateRules(dto.getRules());
// 2. 创建活动主记录
PromotionFull promotion = new PromotionFull();
promotion.setPromotionName(dto.getName());
promotion.setPromotionType(dto.getType());
promotion.setPromotionLevel(2); // 商家级
promotion.setMerchantId(merchantId);
promotion.setScopeType(dto.getScopeType());
promotion.setStackableCoupon(dto.getStackableCoupon());
promotion.setStartTime(dto.getStartTime());
promotion.setEndTime(dto.getEndTime());
promotion.setAuditStatus(1); // 待审核
promotion.setStatus(0);
promotionMapper.insert(promotion);
// 3. 批量插入阶梯规则
List<PromotionFullRule> rules = dto.getRules().stream()
.map(r -> {
PromotionFullRule rule = new PromotionFullRule();
rule.setPromotionId(promotion.getId());
rule.setThresholdAmount(r.getThreshold());
rule.setDiscountType(r.getDiscountType());
rule.setDiscountValue(r.getDiscountValue());
rule.setSort(r.getSort());
return rule;
}).collect(Collectors.toList());
ruleMapper.batchInsert(rules);
// 4. 通知平台审核
applicationEventPublisher.publishEvent(new PromotionToAuditEvent(promotion.getId()));
return promotion.getId();
}
private void validateRules(List<PromotionRuleDTO> rules) {
// 校验阶梯金额递增
for (int i = 1; i < rules.size(); i++) {
if (rules.get(i).getThreshold().compareTo(rules.get(i-1).getThreshold()) <= 0) {
throw new BizException("阶梯金额必须递增");
}
}
// 校验折扣率在0-1之间
// 校验满赠必须指定赠品SKU
}
}
4.2 下单优惠计算(核心)
这是整个模块的心脏:用户提交订单时,自动匹配可用活动,计算最优档位,按比例分摊到各子订单。
@Service
public class PromotionCalcService {
/**
* 计算订单可用的满减优惠
* @param mainOrder 主订单信息,包含多个商家子单
* @return 各子订单分摊的优惠明细
*/
public List<OrderPromotionFullVO> calculate(MainOrderCalcDTO mainOrder) {
List<OrderPromotionFullVO> result = new ArrayList<>();
List<SubOrderCalcDTO> subOrders = mainOrder.getSubOrders();
// 1. 匹配可用的平台级活动(跨店满减)
List<PromotionFull> platformPromotions = matchPlatformPromotions(mainOrder);
if (!platformPromotions.isEmpty()) {
// 取最优活动
PromotionFull bestPlatform = selectBestPromotion(platformPromotions, mainOrder.getTotalAmount());
BigDecimal totalDiscount = calcDiscount(bestPlatform, mainOrder.getTotalAmount());
// 按子订单金额比例分摊
result.addAll(allocateDiscountToSubOrders(
bestPlatform, totalDiscount, subOrders, 1)); // 1=平台承担
}
// 2. 匹配各商家自身的店铺级活动
for (SubOrderCalcDTO subOrder : subOrders) {
List<PromotionFull> shopPromotions = matchShopPromotions(subOrder);
if (shopPromotions.isEmpty()) continue;
// 校验叠加规则:如果平台活动不可叠加商家活动,则跳过
if (!platformPromotions.isEmpty() && !bestPlatform.getStackableOther()) {
continue;
}
PromotionFull bestShop = selectBestPromotion(shopPromotions, subOrder.getAmount());
BigDecimal shopDiscount = calcDiscount(bestShop, subOrder.getAmount());
OrderPromotionFullVO vo = new OrderPromotionFullVO();
vo.setOrderId(subOrder.getOrderId());
vo.setPromotionId(bestShop.getId());
vo.setPromotionLevel(2);
vo.setOrderAmount(subOrder.getAmount());
vo.setDiscountAmount(shopDiscount);
vo.setBearerType(2); // 商家承担
result.add(vo);
}
return result;
}
/**
* 计算对应档位的优惠金额
*/
private BigDecimal calcDiscount(PromotionFull promotion, BigDecimal amount) {
List<PromotionFullRule> rules = getRules(promotion.getId());
// 找到满足条件的最高档位
PromotionFullRule matched = null;
for (PromotionFullRule rule : rules) {
if (amount.compareTo(rule.getThresholdAmount()) >= 0) {
matched = rule;
}
}
if (matched == null) return BigDecimal.ZERO;
switch (promotion.getPromotionType()) {
case 1: // 满减
return matched.getDiscountValue();
case 2: // 满折
return amount.multiply(BigDecimal.ONE.subtract(matched.getDiscountValue()))
.setScale(2, RoundingMode.HALF_UP);
default:
return BigDecimal.ZERO;
}
}
/**
* 按子订单金额比例分摊优惠
*/
private List<OrderPromotionFullVO> allocateDiscountToSubOrders(
PromotionFull promotion, BigDecimal totalDiscount,
List<SubOrderCalcDTO> subOrders, int bearerType) {
List<OrderPromotionFullVO> list = new ArrayList<>();
BigDecimal totalAmount = subOrders.stream()
.map(SubOrderCalcDTO::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal allocated = BigDecimal.ZERO;
for (int i = 0; i < subOrders.size(); i++) {
SubOrderCalcDTO sub = subOrders.get(i);
BigDecimal discount;
if (i == subOrders.size() - 1) {
// 最后一子单用倒算,吸收精度误差
discount = totalDiscount.subtract(allocated);
} else {
BigDecimal ratio = sub.getAmount().divide(totalAmount, 4, RoundingMode.HALF_UP);
discount = totalDiscount.multiply(ratio)
.setScale(2, RoundingMode.HALF_UP);
allocated = allocated.add(discount);
}
OrderPromotionFullVO vo = new OrderPromotionFullVO();
vo.setOrderId(sub.getOrderId());
vo.setPromotionId(promotion.getId());
vo.setPromotionLevel(1);
vo.setOrderAmount(sub.getAmount());
vo.setDiscountAmount(discount);
vo.setBearerType(bearerType);
list.add(vo);
}
return list;
}
}
4.3 订单创建写入优惠快照
计算完成后,将优惠明细持久化,作为后续分账、退款、对账的依据。
@Transactional(rollbackFor = Exception.class)
public void saveOrderPromotions(Long mainOrderId, List<OrderPromotionFullVO> details) {
for (OrderPromotionFullVO detail : details) {
OrderPromotionFull record = new OrderPromotionFull();
record.setOrderId(detail.getOrderId());
record.setMainOrderId(mainOrderId);
record.setPromotionId(detail.getPromotionId());
record.setPromotionLevel(detail.getPromotionLevel());
record.setOrderAmount(detail.getOrderAmount());
record.setDiscountAmount(detail.getDiscountAmount());
record.setBearerType(detail.getBearerType());
record.setCreateTime(new Date());
orderPromotionMapper.insert(record);
// 更新子订单优惠汇总字段
orderMapper.addPromotionDiscount(detail.getOrderId(), detail.getDiscountAmount());
}
}
五、上下游模块联动
满减满赠不是孤立的营销工具,必须和订单、分账、退款、库存等模块联动,才能保证账务一致。
5.1 分账模块联动
这是多商户场景最关键的联动点,优惠承担方不同,分账处理方式完全不同:
- 商家承担的优惠:从商家货款中扣除,平台佣金按实付金额计算;
- 平台承担的优惠:不减少商家货款,平台从佣金收入中抵扣,相当于平台补贴。
分账计算时,统一以「子订单实付金额」为基数,即原价 - 商家优惠 - 平台优惠分摊,再按佣金比例计算平台收入。
5.2 退款模块联动
退款时必须同步回退对应优惠,避免用户多次退款套取优惠:
- 整单退款:该子订单对应分摊的优惠全额回退,活动名额释放;
- 部分退款:按退款金额占比,等比例回退优惠金额;
- 满赠退款:赠品需一并退回,否则扣除赠品金额。
5.3 库存模块联动
- 满减、满折不影响库存,仅影响金额;
- 满赠活动的赠品,需要单独扣减赠品 SKU 库存;用户退款时赠品退回,库存同步回补。
5.4 商家权限联动
- 商家只能创建、查看、管理自己店铺的活动;
- 平台可以查看全平台活动,执行审核、下架操作;
- 所有状态变更留操作日志,全程可追溯。
5.5 定时任务联动
- 活动定时开始、定时结束,自动更新状态;
- 每日统计各活动的参与订单数、GMV、优惠总金额,输出报表;
- 到期活动自动下架,不再参与下单计算。
六、平滑升级与兼容方案
6.1 升级步骤
- 执行新增表 SQL 与订单扩展字段 SQL,全部为新增操作,不破坏原生数据;
- 部署营销模块独立代码,原生订单、分账代码仅做少量扩展;
- 后台先灰度开放给部分商家试用,验证计算与分账准确性;
- 全量放开后,同步更新商家后台活动创建指引。
6.2 历史数据兼容
- 历史订单没有活动关联记录,默认视为未参与活动,不影响原有数据;
- 统计报表支持「含活动」「不含活动」两种口径,保证数据连续性;
- 商家原有其他营销活动不受影响,叠加规则默认按原生逻辑执行。
6.3 官方版本升级兼容
- 核心改造以新增表、新增服务为主,极少修改原生核心文件;
- 官方版本迭代时,重点核对订单金额字段、分账计算逻辑是否有变动,同步适配即可;
- 独立的计算服务层将营销逻辑与订单逻辑解耦,官方升级对二开部分的影响降到最低。
七、常见坑与避坑建议
1. 优惠叠加失控
现象:平台满减 + 店铺满减 + 优惠券同时生效,优惠总额超过商品金额,出现 0 元单甚至负金额。
避坑:建立叠加规则矩阵,每类活动都配置明确的互斥关系;下单计算前统一校验叠加权限,最终优惠金额设置下限保护。
2. 分摊精度误差
现象:各子订单分摊优惠相加,与总优惠金额差几分钱,导致分账对账对不上。
避坑:最后一个子订单用「总优惠 - 已分摊总额」倒算,把精度误差吸收在最后一笔;对账以优惠总金额为准,不以各子单相加为准。
3. 退款优惠回退不全
现象:用户退部分款,优惠金额没按比例扣回,相当于用户用更少的钱享受了同样的优惠。
避坑:部分退款严格按退款金额占原订单金额的比例,等比例回退优惠;回退后更新订单优惠快照记录。
4. 活动时间边界并发
现象:活动结束瞬间下单,计算时活动有效,提交时活动已结束,导致状态不一致。
避坑:下单扣减库存前,二次校验活动状态与时间;数据库层面用条件更新保证原子性。
5. 商家虚高原价再满减
现象:商家先抬高商品原价,再做满减活动,虚假促销引发投诉。
避坑:平台审核环节增加原价校验,对比商品历史均价;设置活动价格保护期,活动前一段时间内价格不得大幅上调。
结语
多商户的满减满赠,表面看是加个优惠计算,本质是「平台-商家-用户」三方的账务分配问题。
LikeShop 多商户高级版原生已经有扎实的订单与分账底座,二开时不用推翻重构,通过新增独立活动体系、统一计算入口、事件联动上下游的方式,可以用很低的侵入成本实现灵活的营销玩法,同时保证资金账务的准确性。
对二开团队来说,营销类功能最容易越写越乱,核心就是守住「计算统一入口、账务快照留痕、上下游事件联动」这三条原则。架构做清晰了,后续再加拼团、砍价、优惠券等更多营销工具,也都能有序地嵌套进这套体系里。