不怕庫存不同步!用 Java + Spring Boot 每 5 分鐘自動校準(zhǔn)外賣平臺庫存!
對于中小商戶而言,庫存差異是外賣與線下經(jīng)營中的“隱形殺手”。 線下賣完、線上還在售——導(dǎo)致“缺貨訂單”; 線上訂單多、線下庫存沒更新——結(jié)果“超賣賠單”。
為了解決這一痛點,我們基于 Java + Spring Boot 構(gòu)建了一套可落地的 “庫存雙向同步系統(tǒng)”。 系統(tǒng)以 5 分鐘為同步周期,自動檢測外賣平臺(美團(tuán)/餓了么)與線下收銀系統(tǒng)(商米、思迅或 Excel 導(dǎo)入)的庫存差異,實現(xiàn) “線下→線上” 與 “線上→線下” 的雙向數(shù)據(jù)一致性。
系統(tǒng)整體架構(gòu)(輕量三層結(jié)構(gòu))
項目目錄結(jié)構(gòu)
/usr/local/java/sync
├── src/main/java/com/icoderoad/inventory
│ ├── controller
│ ├── service
│ ├── mapper
│ └── entity
├── src/main/resources
│ ├── application.yml
│ └── mapper/*.xml
└── pom.xml系統(tǒng)架構(gòu)采用典型的三層模型:
┌─────────────────┐ ┌────────────────────────────────┐ ┌────────────────────────┐
│ 微信小程序 │?────?│ Spring Boot 后端服務(wù)(MySQL、定時任務(wù))│?────?│ 外賣平臺 / 收銀系統(tǒng) API │
│(配置入口) │ │ │ │ 美團(tuán) / 餓了么 / 商米等 │
└─────────────────┘ └────────────────────────────────┘ └────────────────────────┘設(shè)計目標(biāo):
- 輕量化部署:無復(fù)雜中間件依賴
- 模塊化職責(zé)清晰:映射管理、庫存同步、日志追蹤
- 可平滑擴(kuò)展:支持 Excel 導(dǎo)入、掃碼匹配、實時推送
數(shù)據(jù)庫設(shè)計
商品映射表(product_mapping)
用于建立 “線下商品” 與 “線上商品” 的對應(yīng)關(guān)系。
CREATE TABLE `product_mapping` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`shop_id` VARCHAR(50) NOT NULL COMMENT '店鋪ID',
`offline_product_id` VARCHAR(50) NOT NULL COMMENT '線下商品ID',
`offline_product_name` VARCHAR(100) NOT NULL COMMENT '線下商品名稱',
`platform_type` TINYINT NOT NULL COMMENT '平臺類型:1=美團(tuán),2=餓了么',
`online_product_id` VARCHAR(50) NOT NULL COMMENT '線上商品ID',
`online_product_name` VARCHAR(100) NOT NULL COMMENT '線上商品名稱',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_shop_offline_platform` (`shop_id`,`offline_product_id`,`platform_type`)
) COMMENT='商品映射表';庫存記錄表(inventory_record)
記錄線下與線上庫存數(shù)據(jù)的同步結(jié)果。
CREATE TABLE `inventory_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`shop_id` VARCHAR(50) NOT NULL COMMENT '店鋪ID',
`offline_product_id` VARCHAR(50) NOT NULL COMMENT '線下商品ID',
`current_stock` INT NOT NULL DEFAULT 0 COMMENT '可售庫存',
`offline_actual_stock` INT NOT NULL DEFAULT 0 COMMENT '線下實際庫存',
`online_occupied_stock` INT NOT NULL DEFAULT 0 COMMENT '線上未發(fā)貨訂單占用',
`last_sync_time` DATETIME DEFAULT NULL COMMENT '最后同步時間',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_shop_offline` (`shop_id`,`offline_product_id`)
) COMMENT='庫存記錄表';同步日志表(sync_log)
記錄同步任務(wù)執(zhí)行情況。
CREATE TABLE `sync_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`shop_id` VARCHAR(50) NOT NULL,
`sync_type` TINYINT NOT NULL COMMENT '1=線上→線下, 2=線下→線上, 3=手動同步',
`sync_result` TINYINT NOT NULL COMMENT '0=失敗, 1=成功',
`message` VARCHAR(500) DEFAULT NULL COMMENT '失敗原因',
`sync_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_shop_time` (`shop_id`,`sync_time`)
) COMMENT='同步日志表';核心業(yè)務(wù)模塊實現(xiàn)
商品映射管理模塊(ProductMappingController)
package com.icoderoad.inventory.controller;
@RestController
@RequestMapping("/api/product-mapping")
public class ProductMappingController {
@Autowired
private ProductMappingService mappingService;
// 添加映射關(guān)系
@PostMapping("/add")
public Result<Void> addMapping(@RequestBody ProductMappingDTO dto) {
mappingService.addMapping(dto);
return Result.success();
}
// 查詢映射列表
@GetMapping("/list")
public Result<List<ProductMappingVO>> getMappingList(@RequestParam String shopId) {
return Result.success(mappingService.listByShopId(shopId));
}
}庫存同步引擎(核心邏輯)
(1)拉取線上訂單
package com.icoderoad.inventory.service;
@Service
public class OnlineOrderService {
// 獲取美團(tuán)5分鐘內(nèi)新訂單
public List<MeituanOrder> pullMeituanOrders(String shopId, LocalDateTime start, LocalDateTime end) {
String sign = generateSign(shopId, start, end);
String url = "https://open-meituan.com/api/v1/order/list?shop_id=" + shopId
+ "&start_time=" + start + "&end_time=" + end + "&sign=" + sign;
String response = HttpClientUtil.get(url);
return JSON.parseArray(response, MeituanOrder.class);
}
// 拉取餓了么訂單(同理實現(xiàn))
}(2)讀取線下庫存(API 或 Excel)
@Service
public class OfflineStockService {
// 調(diào)用商米API獲取庫存
public List<OfflineStock> getShangmiStock(String shopId) {
String url = "https://shangmi-api.com/stock?shop_id=" + shopId;
String response = HttpClientUtil.get(url);
return JSON.parseArray(response, OfflineStock.class);
}
// 解析 Excel 文件
public List<OfflineStock> parseExcelStock(MultipartFile file) throws IOException {
List<OfflineStock> stocks = new ArrayList<>();
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getSheetAt(0);
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
stocks.add(new OfflineStock(
row.getCell(0).getStringCellValue(),
(int) row.getCell(1).getNumericCellValue()
));
}
}
return stocks;
}
}(3)雙向同步核心邏輯
@Service
@Transactional
public class InventorySyncService {
@Autowired private OnlineOrderService onlineOrderService;
@Autowired private OfflineStockService offlineStockService;
@Autowired private InventoryRecordMapper inventoryMapper;
@Autowired private ProductMappingService mappingService;
// 線上→線下
public void syncOnlineToOffline(String shopId) {
LocalDateTime end = LocalDateTime.now();
LocalDateTime start = end.minusMinutes(5);
List<MeituanOrder> mtOrders = onlineOrderService.pullMeituanOrders(shopId, start, end);
List<ElemeOrder> eleOrders = onlineOrderService.pullElemeOrders(shopId, start, end);
Map<String, Integer> occupied = calculateOnlineOccupied(mtOrders, eleOrders, shopId);
occupied.forEach((pid, num) -> inventoryMapper.updateOnlineOccupied(shopId, pid, num));
}
// 線下→線上
public void syncOfflineToOnline(String shopId) {
List<OfflineStock> stocks = offlineStockService.getLatestOfflineStock(shopId);
for (OfflineStock s : stocks) {
InventoryRecord record = inventoryMapper.getByShopAndProduct(shopId, s.getProductId());
int available = record.getOfflineActualStock() - record.getOnlineOccupiedStock();
syncToPlatform(shopId, s.getProductId(), available);
inventoryMapper.updateCurrentStock(shopId, s.getProductId(), available);
}
}
private void syncToPlatform(String shopId, String offlinePid, int stock) {
List<ProductMapping> mappings = mappingService.getByOfflineProduct(shopId, offlinePid);
for (ProductMapping m : mappings) {
if (m.getPlatformType() == 1) meituanApi.updateStock(m.getOnlineProductId(), stock);
if (m.getPlatformType() == 2) elemeApi.updateStock(m.getOnlineProductId(), stock);
}
}
}定時任務(wù) + 手動觸發(fā)模塊
@Service
public class SyncTask {
@Autowired private InventorySyncService syncService;
@Autowired private SyncLogService syncLogService;
@Autowired private ShopService shopService;
// 每5分鐘執(zhí)行
@Scheduled(cron = "0 0/5 * * * ?")
public void autoSync() {
for (String shopId : shopService.getAllShopIds()) {
try {
syncService.syncOnlineToOffline(shopId);
syncService.syncOfflineToOnline(shopId);
syncLogService.saveLog(shopId, 3, 1, "定時同步成功");
} catch (Exception e) {
syncLogService.saveLog(shopId, 3, 0, "同步失敗:" + e.getMessage());
}
}
}
// 手動同步(微信小程序入口)
public void manualSync(String shopId) {
try {
syncService.syncOnlineToOffline(shopId);
syncService.syncOfflineToOnline(shopId);
syncLogService.saveLog(shopId, 3, 1, "手動同步成功");
} catch (Exception e) {
syncLogService.saveLog(shopId, 3, 0, "手動同步失?。? + e.getMessage());
throw new BusinessException("同步失敗,請稍后重試");
}
}
}庫存狀態(tài)可視化后臺界面
為了讓運(yùn)營人員能夠直觀了解庫存同步情況,我們設(shè)計了一個基于 Thymeleaf + Bootstrap + Chart.js 的后臺頁面,實現(xiàn)如下功能:
- 表格展示當(dāng)前庫存差異;
- 動態(tài)條形圖顯示各商品庫存狀態(tài);
- 每 5 分鐘自動刷新一次數(shù)據(jù)。
Controller 層
package com.icoderoad.stock.controller;
import com.icoderoad.stock.repository.ProductStockRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequiredArgsConstructor
public class StockViewController {
private final ProductStockRepository repository;
/** 訪問后臺庫存頁面 */
@GetMapping("/admin/stock")
public String stockPage(Model model) {
model.addAttribute("stocks", repository.findAll());
return "stock-view";
}
/** 提供庫存數(shù)據(jù)接口(Chart.js 拉取用) */
@GetMapping("/admin/stock/data")
@ResponseBody
public Object stockData() {
return repository.findAll();
}
}Thymeleaf 頁面:src/main/resources/templates/stock-view.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>庫存狀態(tài)可視化后臺</title>
<link rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="bg-light">
<div class="container mt-5">
<h3 class="mb-4 text-center">?? 外賣平臺庫存同步監(jiān)控后臺</h3>
<!-- 表格展示 -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title mb-3">庫存詳情</h5>
<table class="table table-striped table-bordered">
<thead class="table-dark">
<tr>
<th>商品編碼</th>
<th>商品名稱</th>
<th>本地庫存</th>
<th>平臺庫存</th>
<th>最后同步時間</th>
</tr>
</thead>
<tbody>
<tr th:each="s : ${stocks}">
<td th:text="${s.productCode}"></td>
<td th:text="${s.productName}"></td>
<td th:text="${s.localStock}"></td>
<td th:text="${s.remoteStock}"></td>
<td th:text="${s.updateTime}"></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Chart.js 圖表 -->
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title mb-3">庫存對比圖</h5>
<canvas id="stockChart" height="100"></canvas>
</div>
</div>
</div>
<script>
const ctx = document.getElementById('stockChart');
async function loadChartData() {
const res = await fetch('/admin/stock/data');
const data = await res.json();
const labels = data.map(d => d.productName);
const localStocks = data.map(d => d.localStock);
const remoteStocks = data.map(d => d.remoteStock);
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: '本地庫存',
data: localStocks,
borderWidth: 1
},
{
label: '平臺庫存',
data: remoteStocks,
borderWidth: 1
}
]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true }
}
}
});
}
loadChartData();
// 每5分鐘刷新一次數(shù)據(jù)
setInterval(loadChartData, 300000);
</script>
</body>
</html>微信小程序端設(shè)計
頁面 | 功能說明 |
商品映射頁 | 維護(hù)線下與線上商品對應(yīng)關(guān)系 |
庫存狀態(tài)頁 | 展示實時庫存與“立即同步”操作 |
同步日志頁 | 顯示最近 10 條同步結(jié)果與錯誤原因 |
基于 uni-app 實現(xiàn),一套代碼支持微信與支付寶小程序。
關(guān)鍵技術(shù)要點與避坑建議
- 平臺 API 調(diào)用簽名
- 美團(tuán)與餓了么需申請
appKey、secret; - 請求參數(shù)需按時間戳、簽名算法嚴(yán)格生成;
- 控制 API 調(diào)用頻率,避免被限流。
- 庫存計算準(zhǔn)確性
- 僅統(tǒng)計“已支付未發(fā)貨”訂單;
- Excel 導(dǎo)入庫存需記錄時間戳,以最新數(shù)據(jù)為準(zhǔn)。
- 異常與重試機(jī)制
- 使用
@Retryable實現(xiàn) API 自動重試; - 庫存為負(fù)時提示“庫存不足”,并記錄日志。
- 安全與配置隔離
- 使用
token認(rèn)證小程序用戶; - 敏感信息通過環(huán)境變量或 Spring Config 存儲。
部署方案與成本控制
項目 | 方案 | 年成本 |
服務(wù)器 | 阿里云輕量應(yīng)用服務(wù)器(2核4G) | ¥1000 |
數(shù)據(jù)庫 | MySQL 8.0(同機(jī)部署) | ¥0 |
小程序認(rèn)證 | 微信官方認(rèn)證 | ¥300 |
開發(fā)成本 | 個人開發(fā) 2–3 周 | 約 ¥5000 |
?? 初期總成本約 ¥1300 / 年,適合小微商戶快速上線。
驗證與商業(yè)化路徑
冷啟動驗證 選擇 3–5 家便利店免費(fèi)試用,收集反饋。
迭代優(yōu)化
- 掃碼匹配商品條碼
- 縮短同步間隔(3 分鐘)
- 接入“訂單推送 API”實現(xiàn)實時同步
盈利模式 按店鋪收取月費(fèi) ¥50,30 家店即可覆蓋全部成本。
結(jié)論:讓庫存同步真正“自動化、可信賴”
這套基于 Spring Boot + 定時任務(wù) + 平臺 API 對接 的方案,用極低的開發(fā)與運(yùn)維成本,實現(xiàn)了穩(wěn)定、精準(zhǔn)、可擴(kuò)展的庫存雙向同步。 它不僅節(jié)省了人工更新成本,更避免了庫存誤差導(dǎo)致的差評和損失。
先讓 1 家店同步跑通,再放大規(guī)模——這是中小商戶數(shù)字化轉(zhuǎn)型最務(wù)實的路徑。 用熟悉的 Java 技術(shù)棧,你就能輕松打造一個“自動校準(zhǔn)庫存的隱形員工”。


























