Loading...
Loading...
Compare original and translation side by side
| Platform | Built-in Inventory | Recommended Extension |
|---|---|---|
| Shopify | Native per-variant inventory tracking with location support | Stocky (free, by Shopify) for purchase orders and demand forecasting |
| WooCommerce | Native stock management with backorder support | ATUM Inventory Management for advanced multi-warehouse and supplier POs |
| BigCommerce | Native per-SKU inventory tracking with low-stock alerts | Multi-Location Inventory app for warehouse routing |
| Custom / Headless | Build atomic reservation with optimistic locking | Required for custom platforms without native inventory management |
| 平台 | 内置库存功能 | 推荐扩展 |
|---|---|---|
| Shopify | 支持按变体、按地点的原生库存追踪 | Stocky(Shopify官方免费工具),用于采购订单和需求预测 |
| WooCommerce | 支持缺货预订的原生库存管理 | ATUM Inventory Management,用于高级多仓库和供应商采购订单管理 |
| BigCommerce | 支持按SKU的原生库存追踪及低库存提醒 | Multi-Location Inventory应用,用于仓库路由 |
| 自定义/无头平台 | 使用乐观锁构建原子化库存预留 | 无原生库存管理的自定义平台必备 |
// lib/inventory.ts
const MAX_RETRIES = 3;
// Reserve inventory atomically — handles concurrent requests safely
export async function reserveInventory({
variantId, locationId, quantity, referenceId
}: { variantId: string; locationId: string; quantity: number; referenceId: string }) {
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const level = await db.inventoryLevels.findUnique({
where: { variantId_locationId: { variantId, locationId } },
});
if (!level) throw new Error(`Inventory not found: ${variantId}`);
const available = level.onHand - level.reserved;
if (available < quantity && !level.backorderAllowed) {
throw new Error(`Insufficient stock: ${available} available, ${quantity} requested`);
}
// Optimistic update — only succeeds if version hasn't changed (no concurrent modifications)
const updated = await db.inventoryLevels.updateMany({
where: { variantId_locationId: { variantId, locationId }, version: level.version },
data: { reserved: level.reserved + quantity, version: level.version + 1 },
});
if (updated.count === 0) {
// Another process modified inventory concurrently; retry
await new Promise(r => setTimeout(r, 50 * (attempt + 1)));
continue;
}
// Log the transaction for audit trail
await db.inventoryTransactions.create({
data: { variantId, locationId, type: 'reserve', quantity: -quantity, referenceId },
});
return { success: true, remaining: available - quantity };
}
throw new Error(`Failed to reserve inventory after ${MAX_RETRIES} retries`);
}
// Release reservation when cart expires or order is cancelled
export async function releaseReservation({
variantId, locationId, quantity, referenceId
}: { variantId: string; locationId: string; quantity: number; referenceId: string }) {
// Idempotency check — don't release twice
const existing = await db.inventoryTransactions.findFirst({
where: { type: 'release', referenceId, variantId },
});
if (existing) return;
await db.$transaction([
db.inventoryLevels.update({
where: { variantId_locationId: { variantId, locationId } },
data: { reserved: { decrement: quantity } },
}),
db.inventoryTransactions.create({
data: { variantId, locationId, type: 'release', quantity: +quantity, referenceId },
}),
]);
}
// Expire stale cart reservations — run every 5-10 minutes via cron
export async function expireStaleCartReservations() {
const TTL_MINUTES = 30;
const cutoff = new Date(Date.now() - TTL_MINUTES * 60 * 1000);
const staleCarts = await db.carts.findMany({
where: { status: 'active', updatedAt: { lt: cutoff }, reservedAt: { not: null } },
include: { items: true },
});
for (const cart of staleCarts) {
for (const item of cart.items) {
await releaseReservation({ variantId: item.variantId, locationId: item.locationId, quantity: item.quantity, referenceId: cart.id });
}
}
}// lib/inventory.ts
const MAX_RETRIES = 3;
// Reserve inventory atomically — handles concurrent requests safely
export async function reserveInventory({
variantId, locationId, quantity, referenceId
}: { variantId: string; locationId: string; quantity: number; referenceId: string }) {
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const level = await db.inventoryLevels.findUnique({
where: { variantId_locationId: { variantId, locationId } },
});
if (!level) throw new Error(`Inventory not found: ${variantId}`);
const available = level.onHand - level.reserved;
if (available < quantity && !level.backorderAllowed) {
throw new Error(`Insufficient stock: ${available} available, ${quantity} requested`);
}
// Optimistic update — only succeeds if version hasn't changed (no concurrent modifications)
const updated = await db.inventoryLevels.updateMany({
where: { variantId_locationId: { variantId, locationId }, version: level.version },
data: { reserved: level.reserved + quantity, version: level.version + 1 },
});
if (updated.count === 0) {
// Another process modified inventory concurrently; retry
await new Promise(r => setTimeout(r, 50 * (attempt + 1)));
continue;
}
// Log the transaction for audit trail
await db.inventoryTransactions.create({
data: { variantId, locationId, type: 'reserve', quantity: -quantity, referenceId },
});
return { success: true, remaining: available - quantity };
}
throw new Error(`Failed to reserve inventory after ${MAX_RETRIES} retries`);
}
// Release reservation when cart expires or order is cancelled
export async function releaseReservation({
variantId, locationId, quantity, referenceId
}: { variantId: string; locationId: string; quantity: number; referenceId: string }) {
// Idempotency check — don't release twice
const existing = await db.inventoryTransactions.findFirst({
where: { type: 'release', referenceId, variantId },
});
if (existing) return;
await db.$transaction([
db.inventoryLevels.update({
where: { variantId_locationId: { variantId, locationId } },
data: { reserved: { decrement: quantity } },
}),
db.inventoryTransactions.create({
data: { variantId, locationId, type: 'release', quantity: +quantity, referenceId },
}),
]);
}
// Expire stale cart reservations — run every 5-10 minutes via cron
export async function expireStaleCartReservations() {
const TTL_MINUTES = 30;
const cutoff = new Date(Date.now() - TTL_MINUTES * 60 * 1000);
const staleCarts = await db.carts.findMany({
where: { status: 'active', updatedAt: { lt: cutoff }, reservedAt: { not: null } },
include: { items: true },
});
for (const cart of staleCarts) {
for (const item of cart.items) {
await releaseReservation({ variantId: item.variantId, locationId: item.locationId, quantity: item.quantity, referenceId: cart.id });
}
}
}inventory_transactionsinventory_transactions| Problem | Solution |
|---|---|
| Overselling during flash sales on Shopify | Shopify's checkout holds inventory during the checkout flow; for very high-concurrency launches, set purchase limits using Shopify Scripts (Plus) or use a waitlist app |
| WooCommerce inventory not decremented after order | Check that stock management is enabled on the product AND globally in WooCommerce settings; both must be on |
| Inventory released immediately when order is cancelled before fulfillment | This is correct behavior for physical goods — released inventory becomes available for other customers; only delay release for backordered items |
| Negative inventory after manual adjustment | Add validation in WooCommerce (ATUM) or set a DB check constraint on custom builds; |
| Shopify location not receiving inventory updates from POS | Ensure POS is connected to the correct Shopify location in Settings → Locations → POS channel |
| 问题 | 解决方案 |
|---|---|
| Shopify闪购期间出现超卖 | Shopify的结账流程会在结账过程中锁定库存;对于极高并发的发布活动,使用Shopify Scripts(Plus版)设置购买限额,或使用等待列表应用 |
| WooCommerce订单完成后库存未扣减 | 检查商品和WooCommerce全局设置中是否都启用了库存管理;两者必须同时开启 |
| 订单在履约前取消,库存立即释放 | 对于实物商品,这是正确的行为——释放的库存可供其他顾客购买;仅对缺货预订商品延迟释放 |
| 手动调整后出现负库存 | 在WooCommerce(ATUM)中添加验证,或在自定义构建中设置数据库检查约束;确保 |
| Shopify地点未收到POS的库存更新 | 确保POS在Settings → Locations → POS channel中连接到正确的Shopify地点 |