linlinjava/litemall · critical · RuntimeException
商品货品库存减少失败
Error message
商品货品库存减少失败
What it means
In WxOrderService.submit, after the stock-availability check passes, the service calls productService.reduceStock(productId, number), which maps to a conditional SQL UPDATE (litemall_goods_product SET number = number - ? ...). The mapper returns the number of affected rows; 0 means the UPDATE matched no row — the product was deleted, the row is logically deleted, or stock changed underneath between the read and the write. The service treats 0 as fatal with '商品货品库存减少失败' (stock reduction failed). It fires after money-facing state is being written, so the surrounding transaction must roll the whole submission back.
Source
Thrown at litemall-wx-api/src/main/java/org/linlinjava/litemall/wx/service/WxOrderService.java:435
// 删除购物车里面的商品信息
if(cartId.equals(0)){
cartService.clearGoods(userId);
}else{
cartService.deleteById(cartId);
}
// 商品货品数量减少
for (LitemallCart checkGoods : checkedGoodsList) {
Integer productId = checkGoods.getProductId();
LitemallGoodsProduct product = productService.findById(productId);
int remainNumber = product.getNumber() - checkGoods.getNumber();
if (remainNumber < 0) {
throw new RuntimeException("下单的商品货品数量大于库存量");
}
if (productService.reduceStock(productId, checkGoods.getNumber()) == 0) {
throw new RuntimeException("商品货品库存减少失败");
}
}
// 如果使用了优惠券,设置优惠券使用状态
if (couponId != 0 && couponId != -1) {
LitemallCouponUser couponUser = couponUserService.findById(userCouponId);
couponUser.setStatus(CouponUserConstant.STATUS_USED);
couponUser.setUsedTime(LocalDateTime.now());
couponUser.setOrderId(orderId);
couponUserService.update(couponUser);
}
//如果是团购项目,添加团购信息
if (grouponRulesId != null && grouponRulesId > 0) {
LitemallGroupon groupon = new LitemallGroupon();
groupon.setOrderId(orderId);
groupon.setStatus(GrouponConstant.STATUS_NONE);
groupon.setUserId(userId);View on GitHub (pinned to a1ef964a71)
Solutions
- Map this failure to the same user-facing out-of-stock response as error [2] (fail the submit with a stock-insufficient code and message), since in practice 0 rows almost always means stock vanished concurrently.
- Make the flow atomic: drop the pre-read check and rely solely on the conditional reduceStock rowcount inside the transaction, as shown below — this removes the race window entirely.
- If it persists deterministically for one SKU, inspect litemall_goods_product for that id: deleted flag, actual number, and confirm the product still exists.
Example fix
// before
if (productService.reduceStock(productId, checkGoods.getNumber()) == 0) {
throw new RuntimeException("商品货品库存减少失败");
}
// after - treat as out-of-stock, message tells the user what to do
if (productService.reduceStock(productId, checkGoods.getNumber()) == 0) {
throw new RuntimeException("下单的商品货品数量大于库存量");
} Defensive patterns
Strategy: validation
Validate before calling
// atomic guard: rely on the conditional UPDATE's row count as the single check
int updated = productService.reduceStock(productId, checkGoods.getNumber()); // UPDATE ... WHERE number >= #{num} AND deleted = false
if (updated == 0) {
// out of stock (or row gone) — abort this SKU, do not pre-read product.getNumber()
} Try / catch
// keep the whole submit inside one @Transactional method; a throw on 0 rows rolls back every prior reduceStock in the same loop
try {
order = wxOrderService.submit(userId, body);
} catch (RuntimeException e) {
TransactionAspectSupport.currentTransactionStatus().isRollbackOnly(); // assert rollback
return ResponseUtil.fail(GOODS_OUT_OF_STOCK, "商品库存不足");
} Prevention
- Never trust a stock value read earlier in the same request; make the decrement conditional in SQL.
- Keep the entire checked-goods loop inside one transaction so a failure on SKU N rolls back SKUs 1..N-1.
When it happens
Trigger: Two requests race: both pass the remainNumber check for the last unit, one reduceStock UPDATE wins, the other affects 0 rows; or the SKU row was logically deleted (deleted=1) / admin-removed between the cart read and checkout; or the reduceStock WHERE clause (id match, not-deleted, sufficient number) fails for any reason.
Common situations: Concurrent checkout of the same popular SKU (groupon/flash sale); product taken off-shelf while sitting in a user's cart; DB replication lag in split deployments; tests running without a real DB so the UPDATE returns 0.
Related errors
AI-assisted analysis of linlinjava/litemall@a1ef964a71 (2026-08-14).
Data as JSON: /api/errors/3bfa87cc4d5ed4c4.
Report an issue: GitHub.