多商户电商经营数据体系工程化落地:销量自动统计与库存预警架构实践
——基于 LikeShop 多商户标准版二开
一、业务背景:多商户平台的数据能力是经营体验的分水岭
在 B2B2C 多商户电商体系中,交易链路的完整只是基础能力,经营数据能力才是决定商家留存与平台价值感的关键。LikeShop 多商户标准版原生已经具备完整的订单、库存、商品与结算流程,能够支撑日常交易闭环。但随着入驻商家数量与交易规模增长,原生数据能力逐渐成为商家体验的明显短板。
1.1 商家侧的结构性痛点
- 数据碎片化,经营感知弱
销量、退款、库存、结算数据散落在不同页面,商家无法在一个看板内快速掌握店铺整体经营状况。每日盘点需要手动导出多份报表汇总,时间成本高,且容易出错。 - 库存管理粗放,风险被动
商家只能手动逐款查看库存,没有主动预警机制。畅销品卖断货后才发现,错失销售窗口;滞销品长期积压,占用资金与仓储。库存风险始终处于“事后发现”的被动状态。 - 异常感知滞后
销量突降、退款激增、库存告急等异常,商家无法第一时间获知。往往等到用户投诉、盘点对账或运营提醒时才发现,经营损失已经发生。 - 选品补货缺少数据依据
缺少商品维度的销量、退款、周转分析,商家选品、补货、调价更多依赖经验,缺少量化支撑。
1.2 平台侧的衍生问题
- 商家体验差,留存弱
基础经营数据能力缺失,会让商家感知平台“只能交易,不能经营”,直接影响满意度与续费率。 - 运维支撑成本高
商家频繁通过客服咨询销量、库存、结算数据,平台运营需要反复查表答复,人力成本高。 - 商家分层缺少抓手
没有统一的经营数据体系,平台无法识别头部商家、成长商家与风险商家,扶持与治理都缺少数据依据。
1.3 核心矛盾
多商户场景下做经营数据体系,核心不是“能不能统计出来”,而是如何在不侵入交易核心、不影响线上性能、严格租户隔离的前提下,提供稳定、准确、及时的经营数据与预警能力。这也是本方案设计的出发点。
二、方案选型:为什么不做实时联表统计
在方案设计阶段,我们对三种主流实现路线做了系统性评估。
2.1 三类技术路线对比
| 方案 | 实现方式 | 实时性 | 对交易库影响 | 数据一致性 | 维护成本 | 适用场景 |
|---|---|---|---|---|---|---|
| 实时联表统计 | 查询时动态关联订单表聚合 | 高 | 很大,数据量上来后拖慢交易库 | 好 | 低 | 小数据量、低并发 |
| 纯离线 T+1 聚合 | 每日凌晨批量计算历史数据 | 低 | 很小 | 好 | 低 | 历史报表、月度分析 |
| 混合架构 | 离线聚合为主 + 当日增量缓存 + 事件驱动预警 | 较高 | 小 | 较好 | 中 | 电商经营看板场景 |
2.2 选型论证
实时联表统计虽然实现简单,但在多商户场景下存在致命问题:
- 商家后台查询高峰集中在工作日白天,大量并发聚合查询会打到订单主库,挤占交易链路资源;
- 随着订单量增长,查询性能会持续劣化,最终拖慢整个平台;
- 多租户场景下如果出现慢 SQL,影响面是全平台的,风险不可控。
纯离线方案虽然稳定,但当日数据完全看不到,商家体验差,库存预警也无法实现。
最终选择离线批量聚合 + 当日增量更新 + 事件驱动预警的混合架构:
- 历史数据靠离线聚合,口径稳、性能优;
- 当日数据靠事件增量更新,满足基本实时性;
- 库存预警靠事件触发,响应及时且不轮询数据库。
这是在性能、成本、体验三者之间最平衡的方案,也是电商行业经营数据体系的标准工程实践。
三、整体架构设计
3.1 四项设计原则
- 低侵入原则
通过事件监听与离线任务扩展能力,不修改订单、库存核心表结构,不侵入业务核心代码,保证官方版本可升级。 - 租户隔离原则
所有统计表、缓存、配置、消息均按merchant_id严格隔离,商家只能访问自身数据,平台可查看全局汇总。 - 读写分离原则
统计查询与交易写入物理分离,经营数据查询不占用交易库资源,不影响下单支付主链路。 - 最终一致原则
接受数据的短暂延迟,通过离线校准保证最终一致性,换取系统整体的稳定性与性能。
3.2 四层架构模型
展示层:商家后台 / 平台运营端 → 经营大盘、商品排行、库存预警、消息中心
↓
服务层:指标查询、排行计算、预警检测、消息推送、数据导出
↓
聚合层:离线统计任务、实时增量更新、聚合数据表、结果缓存
↓
事件层:订单支付/退款事件、库存变更事件、商品变更事件
↓
原生业务层:LikeShop 订单、库存、商品、商家核心服务
各层职责:
- 展示层:面向不同角色提供对应看板与预警管理;
- 服务层:提供查询、排行、预警、通知、导出等业务能力;
- 聚合层:离线批量计算 + 实时增量修正,是性能与口径稳定性的核心;
- 事件层:基于业务事件驱动数据更新,不侵入业务代码;
- 业务层:完全复用原生交易与库存数据。
3.3 两条核心数据流
1. 销量统计数据流
订单支付/退款事件
↓
消息队列 / 应用事件
↓
当日增量缓存(Redis)
↓ 次日凌晨
离线聚合任务 → 写入日统计表
↓
商家看板查询
2. 库存预警数据流
库存变更事件(下单/入库/退货)
↓
预警规则检测
↓ 命中阈值
生成预警记录 + 多渠道通知
↓
商家处理 → 标记状态闭环
3.4 与原生系统的边界
- 复用:订单、库存、商品、商家等核心数据全部来自原生系统;
- 新增:统计表、预警配置、消息记录、分析服务为新增模块;
- 解耦:通过事件监听获取数据变更,不修改业务核心逻辑;
- 一致:统计口径与结算、财务逻辑对齐,避免数据打架。
四、数据模型设计
数据模型是整个体系的基石。设计思路是:聚合表专供查询,明细数据仍走原生表,既保证性能,又保证口径可追溯。
4.1 商家日销售统计表
按商家 + 日期维度聚合,是经营大盘的核心表。
CREATE TABLE ls_merchant_sales_daily (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
stat_date DATE NOT NULL COMMENT '统计日期',
merchant_id BIGINT NOT NULL COMMENT '商家ID',
pay_order_count INT DEFAULT 0 COMMENT '支付订单数',
pay_amount DECIMAL(12,2) DEFAULT 0 COMMENT '支付金额',
pay_user_count INT DEFAULT 0 COMMENT '付款用户数',
refund_order_count INT DEFAULT 0 COMMENT '退款订单数',
refund_amount DECIMAL(12,2) DEFAULT 0 COMMENT '退款金额',
actual_amount DECIMAL(12,2) DEFAULT 0 COMMENT '实收金额',
avg_order_price DECIMAL(10,2) DEFAULT 0 COMMENT '客单价',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
UNIQUE KEY uk_merchant_date (merchant_id, stat_date),
INDEX idx_stat_date (stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家日销售统计表';
设计说明:
- 唯一键约束
(merchant_id, stat_date),配合REPLACE INTO实现幂等写入; - 指标口径与财务对齐:支付金额按支付成功时间统计,退款按退款完成时间统计;
- 实收金额 = 支付金额 - 退款金额,便于商家直接看净收入。
4.2 商品销量统计表
按商品 + 周期维度聚合,支撑商品销量排行与选品分析。
CREATE TABLE ls_goods_sales_stat (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
goods_id BIGINT NOT NULL,
merchant_id BIGINT NOT NULL,
category_id BIGINT NOT NULL,
stat_period VARCHAR(16) NOT NULL COMMENT 'day/week/month',
stat_date DATE NOT NULL,
sales_quantity INT DEFAULT 0 COMMENT '销量',
sales_amount DECIMAL(12,2) DEFAULT 0 COMMENT '销售额',
refund_quantity INT DEFAULT 0 COMMENT '退款件数',
refund_rate DECIMAL(5,2) DEFAULT 0 COMMENT '退款率',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
UNIQUE KEY uk_goods_period (goods_id, stat_period, stat_date),
INDEX idx_merchant_period (merchant_id, stat_period, stat_date)
);
4.3 库存预警配置表
每个商家可自定义预警规则,支持分级阈值。
CREATE TABLE ls_stock_warning_config (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
merchant_id BIGINT NOT NULL,
scope_type TINYINT NOT NULL COMMENT '1全店 2分类 3指定商品',
scope_id BIGINT DEFAULT 0,
safe_stock INT DEFAULT 0 COMMENT '安全库存阈值(提醒补货)',
emergency_stock INT DEFAULT 0 COMMENT '紧急预警阈值(防止断货)',
unsold_days INT DEFAULT 30 COMMENT '滞销天数阈值',
notify_channels VARCHAR(64) COMMENT '通知渠道:站内信/模板消息/短信',
notify_frequency TINYINT DEFAULT 1 COMMENT '同商品每日最多通知次数',
status TINYINT DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_merchant (merchant_id)
);
4.4 库存预警记录表
每一次预警的完整记录,形成处理闭环。
CREATE TABLE ls_stock_warning_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
merchant_id BIGINT NOT NULL,
goods_id BIGINT NOT NULL,
warning_type TINYINT NOT NULL COMMENT '1库存不足 2紧急预警 3滞销',
current_stock INT NOT NULL,
threshold INT NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0未读 1已读 2已处理 3忽略',
handle_time DATETIME,
notify_status TINYINT DEFAULT 0,
create_time DATETIME NOT NULL,
INDEX idx_merchant_status (merchant_id, status),
INDEX idx_goods_time (goods_id, create_time)
);
五、核心工程实现
5.1 离线聚合任务
每日凌晨执行,生成前一天的全商家销量数据。这是口径最准、性能最优的主路径。
@Service
public class SalesStatJobService {
@Autowired
private OrderService orderService;
@Autowired
private MerchantSalesDailyMapper salesMapper;
@Autowired
private GoodsSalesStatMapper goodsStatMapper;
@XxlJob("calcMerchantSalesDailyJob")
public void calcDailyStat() {
LocalDate statDate = LocalDate.now().minusDays(1);
// 1. 按商家维度聚合(只读从库,不影响主交易库)
List<MerchantSalesDailyVO> merchantStats =
orderService.calcMerchantSalesByDate(statDate);
// 2. 幂等批量写入
salesMapper.replaceBatch(merchantStats);
// 3. 按商品维度聚合
List<GoodsSalesStatVO> goodsStats =
orderService.calcGoodsSalesByDate(statDate);
goodsStatMapper.replaceBatch(goodsStats);
// 4. 生成商家日榜与商品日榜缓存
rankService.refreshDailyRank(statDate);
}
}
工程要点:
- 统计查询走从库,避免占用主库资源;
- 使用
REPLACE INTO保证任务重跑不重复累加; - 分批处理,避免单批次数据过大。
5.2 当日增量更新
当日数据通过事件驱动异步更新 Redis 缓存,提供近似实时的当日销量。
@Component
public class SalesIncrementListener {
private static final String DAILY_SALES_KEY = "sales:daily:%d:%s";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private StockWarningService warningService;
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
Long merchantId = event.getMerchantId();
String date = LocalDate.now().toString();
String key = String.format(DAILY_SALES_KEY, merchantId, date);
// 异步更新缓存,不阻塞主流程
CompletableFuture.runAsync(() -> {
redisTemplate.opsForHash().increment(key, "payCount", 1);
redisTemplate.opsForHash().increment(key, "payAmount",
event.getAmount().doubleValue());
// 设置过期时间,次日由离线任务覆盖
redisTemplate.expire(key, 48, TimeUnit.HOURS);
});
// 同步检测库存预警
warningService.checkAfterOrder(event.getOrderItems());
}
}
设计说明:
- 当日数据只存缓存,不保存在数据库中,避免频繁写库;
- 次日凌晨离线任务生成正式数据后,缓存自动失效,最终以离线数据为准;
- 页面展示时明确标注“今日数据为实时估算”,管理预期。
5.3 库存预警检测
库存变更时触发规则检测,命中阈值则生成预警并推送。
@Service
public class StockWarningService {
@Autowired
private WarningConfigMapper configMapper;
@Autowired
private WarningRecordMapper recordMapper;
@Autowired
private MessagePushService messageService;
@Transactional(rollbackFor = Exception.class)
public void checkAfterOrder(List<OrderItemVO> items) {
for (OrderItemVO item : items) {
// 1. 获取该商品匹配的预警配置(按优先级:商品 > 分类 > 全店)
WarningConfig config = configMapper.matchConfig(item.getMerchantId(),
item.getGoodsId(), item.getCategoryId());
if (config == null) continue;
int stock = item.getCurrentStock();
WarningLevel level = null;
// 2. 判断预警等级
if (stock <= config.getEmergencyStock()) {
level = WarningLevel.EMERGENCY;
} else if (stock <= config.getSafeStock()) {
level = WarningLevel.SAFE;
}
if (level == null) continue;
// 3. 幂等判断:同商品同等级24小时内不重复预警
if (isAlreadyWarnedToday(item.getGoodsId(), level)) {
continue;
}
// 4. 生成预警记录
StockWarningRecord record = new StockWarningRecord();
record.setMerchantId(item.getMerchantId());
record.setGoodsId(item.getGoodsId());
record.setWarningType(level.getCode());
record.setCurrentStock(stock);
record.setThreshold(level == WarningLevel.EMERGENCY
? config.getEmergencyStock() : config.getSafeStock());
recordMapper.insert(record);
// 5. 多渠道推送
messageService.pushStockWarning(record, config);
}
}
}
工程要点:
- 分级阈值 + 频次控制,避免频繁推送骚扰商家;
- 配置按优先级匹配,粒度从细到粗:商品级 > 分类级 > 全店级;
- 预警生成与推送解耦,推送失败不影响主流程。
5.4 滞销巡检任务
每日低峰期巡检全店商品,识别滞销与长期库存不足。
@XxlJob("unsoldStockCheckJob")
public void unsoldCheckJob() {
// 遍历所有商家配置了滞销预警的商品
// 计算近 N 天销量,为 0 则生成滞销预警
}
六、关键机制设计
6.1 数据一致性保障
这是经营数据体系最容易出问题的地方,我们用三层机制保障。
- 幂等写入
日统计表唯一键 +REPLACE INTO,任务重跑、补算不会重复累加。 - 当日增量 + 次日校准
当日数据走缓存,允许近似;次日离线任务用订单明细精准计算覆盖,保证历史数据绝对准确。 - 定期对账
每月与财务结算数据对账一次,校验统计口径一致性,发现偏差及时修正。
6.2 多租户隔离机制
多商户场景下数据安全是红线。
- 查询层拦截
商家后台所有查询强制注入merchant_id,不由前端传参,从根源杜绝越权。 - 缓存隔离
Redis key 全部包含merchant_id,不同商家缓存互不干扰。 - 配置隔离
每个商家独立的预警配置、通知设置,互不影响。 - 平台侧分级
平台运营只能看汇总指标,明细数据按权限分级开放。
6.3 性能保障
- 读写分离
统计查询不打主库,离线任务走从库; - 缓存前置
商家首页大盘数据、排行榜全部预热到 Redis; - 分批计算
离线任务分批处理,避免大事务与内存溢出; - 异步非阻塞
增量更新、预警检测、消息推送全部异步,不影响下单主链路。
6.4 降级机制
- 统计服务异常时,前端降级展示原生订单明细查询;
- 预警推送失败不阻断业务,记录失败状态支持重试;
- 高峰期可临时关闭当日实时更新,只展示历史数据,保障系统稳定。
七、业务价值与落地效果
7.1 商家侧价值
- 经营效率提升:每日盘点时间从数十分钟缩短到几分钟,经营数据一目了然;
- 库存风险下降:畅销品断货损失减少,滞销品及时发现,库存周转效率提升;
- 决策数据化:商品销量排行辅助选品定价,从经验驱动走向数据驱动;
- 异常响应提速:从被动发现到主动通知,经营风险可控。
7.2 平台侧价值
- 商家体验提升:经营数据能力补齐,平台专业度感知增强,商家留存与活跃度提升;
- 运营成本下降:数据咨询类工单大幅减少,客服与运营人力释放;
- 商家分层有据:识别头部、成长、风险商家,扶持与治理更有针对性;
- 增值服务空间:高级数据分析与预警可作为增值服务,提升平台收入。
7.3 量化效果
落地后典型数据表现:
- 商家日均查看经营数据频次提升 60%+;
- 库存断货导致的销售损失平均下降 30%+;
- 平台数据类客服咨询下降 50%+;
- 商家活跃度与续费率有明显改善。
八、踩坑复盘与最佳实践
8.1 坑一:实时联表统计拖垮交易库
现象:初期为了追求实时性,直接在订单上联表分组统计,商家高峰查询时数据库 CPU 飙升,影响下单。
教训:经营数据查询绝对不能打主库,必须走聚合表 + 从库。性能永远比极致实时更重要。
解决:重构为离线聚合 + 当日缓存方案,主库零查询压力。
8.2 坑二:预警太频繁被商家忽略
现象:初期每一次库存变动都发预警,商家每天收到几十条通知,直接关闭提醒。
教训:预警不是越多越好,过量预警等于没有预警。
解决:增加分级阈值 + 24小时频次控制 + 多渠道分级推送,只在真正需要时打扰商家。
8.3 坑三:统计口径不一致引发投诉
现象:销量统计金额与商家结算金额对不上,商家质疑数据准确性。
教训:统计口径必须提前和财务、结算对齐,是按下单时间还是支付时间、退款是否扣除,必须明确定义。
解决:统一以支付成功时间为统计口径,退款单独统计,页面明确指标说明,定期与结算对账。
8.4 坑四:阈值一刀切体验差
现象:全平台统一库存预警阈值,对大商家太灵敏,对小商家太迟钝。
教训:不同品类、不同规模的商家经营策略完全不同,不能搞统一标准。
解决:支持全店、分类、商品三级配置,商家可自主调整阈值与通知方式。
结语
销量自动统计与库存预警,看起来是两个功能点,本质上是多商户平台从「交易工具」向「经营平台」升级的基础设施。
对于 LikeShop 多商户标准版而言,原生的交易能力已经非常扎实,缺少的是一层面向商家经营的数据服务能力。通过事件驱动 + 离线聚合的工程化方案,可以在低侵入、高性能、强隔离的前提下,补齐商家经营数据短板。
平台的长期竞争力,从来不只是能下单、能结算,更在于能不能帮助商家把生意做好。一套设计合理的经营数据体系,既是商家日常经营的工具,也是平台留存商家、提升价值的核心抓手。