{"record":{"id":"3bfa87cc4d5ed4c4","repo":"linlinjava/litemall","slug":"error-3bfa87","errorCode":null,"errorMessage":"商品货品库存减少失败","messagePattern":"商品货品库存减少失败","errorType":"exception","errorClass":"RuntimeException","httpStatus":500,"severity":"critical","filePath":"litemall-wx-api/src/main/java/org/linlinjava/litemall/wx/service/WxOrderService.java","lineNumber":435,"sourceCode":"\n        // 删除购物车里面的商品信息\n        if(cartId.equals(0)){\n            cartService.clearGoods(userId);\n        }else{\n            cartService.deleteById(cartId);\n        }\n\n        // 商品货品数量减少\n        for (LitemallCart checkGoods : checkedGoodsList) {\n            Integer productId = checkGoods.getProductId();\n            LitemallGoodsProduct product = productService.findById(productId);\n\n            int remainNumber = product.getNumber() - checkGoods.getNumber();\n            if (remainNumber < 0) {\n                throw new RuntimeException(\"下单的商品货品数量大于库存量\");\n            }\n            if (productService.reduceStock(productId, checkGoods.getNumber()) == 0) {\n                throw new RuntimeException(\"商品货品库存减少失败\");\n            }\n        }\n\n        // 如果使用了优惠券，设置优惠券使用状态\n        if (couponId != 0 && couponId != -1) {\n            LitemallCouponUser couponUser = couponUserService.findById(userCouponId);\n            couponUser.setStatus(CouponUserConstant.STATUS_USED);\n            couponUser.setUsedTime(LocalDateTime.now());\n            couponUser.setOrderId(orderId);\n            couponUserService.update(couponUser);\n        }\n\n        //如果是团购项目，添加团购信息\n        if (grouponRulesId != null && grouponRulesId > 0) {\n            LitemallGroupon groupon = new LitemallGroupon();\n            groupon.setOrderId(orderId);\n            groupon.setStatus(GrouponConstant.STATUS_NONE);\n            groupon.setUserId(userId);","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/linlinjava/litemall/blob/a1ef964a718b7277925b19ea26afe78ea3a1d325/litemall-wx-api/src/main/java/org/linlinjava/litemall/wx/service/WxOrderService.java#L417-L453","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (productService.reduceStock(productId, checkGoods.getNumber()) == 0) {\n    throw new RuntimeException(\"商品货品库存减少失败\");\n}\n\n// after - treat as out-of-stock, message tells the user what to do\nif (productService.reduceStock(productId, checkGoods.getNumber()) == 0) {\n    throw new RuntimeException(\"下单的商品货品数量大于库存量\");\n}","handlingStrategy":"validation","validationCode":"// atomic guard: rely on the conditional UPDATE's row count as the single check\nint updated = productService.reduceStock(productId, checkGoods.getNumber()); // UPDATE ... WHERE number >= #{num} AND deleted = false\nif (updated == 0) {\n    // out of stock (or row gone) — abort this SKU, do not pre-read product.getNumber()\n}","typeGuard":null,"tryCatchPattern":"// keep the whole submit inside one @Transactional method; a throw on 0 rows rolls back every prior reduceStock in the same loop\ntry {\n    order = wxOrderService.submit(userId, body);\n} catch (RuntimeException e) {\n    TransactionAspectSupport.currentTransactionStatus().isRollbackOnly(); // assert rollback\n    return ResponseUtil.fail(GOODS_OUT_OF_STOCK, \"商品库存不足\");\n}","preventionTips":["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."],"tags":["stock","order","concurrency","database","litemall"],"backgroundTag":null,"analyzedSha":"a1ef964a718b7277925b19ea26afe78ea3a1d325","analyzedAt":"2026-08-14T12:39:46.078Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}