linlinjava/litemall · critical · RuntimeException
更新数据已失效
Error message
更新数据已失效
What it means
In WxOrderService's WeChat pay-notify path (notify callback after payment success), when the order has groupon info the service loads the LitemallGroupon row, sets its status to STATUS_ON and calls grouponService.updateById(groupon). updateById is a MyBatis updateByPrimaryKey that returns affected rows; 0 rows means the row was not updated — typically it was logically deleted or concurrently modified. The service throws '更新数据已失效' (data to update has expired). This happens inside the payment callback, so a throw here fails the whole notify processing.
Source
Thrown at litemall-wx-api/src/main/java/org/linlinjava/litemall/wx/service/WxOrderService.java:496
LitemallOrder o = new LitemallOrder();
o.setId(orderId);
o.setOrderStatus(OrderUtil.STATUS_PAY);
orderService.updateSelective(o);
// 支付成功,有团购信息,更新团购信息
LitemallGroupon groupon = grouponService.queryByOrderId(order.getId());
if (groupon != null) {
grouponRules = grouponRulesService.findById(groupon.getRulesId());
//仅当发起者才创建分享图片
if (groupon.getGrouponId() == 0) {
String url = qCodeService.createGrouponShareImage(grouponRules.getGoodsName(), grouponRules.getPicUrl(), groupon);
groupon.setShareUrl(url);
}
groupon.setStatus(GrouponConstant.STATUS_ON);
if (grouponService.updateById(groupon) == 0) {
throw new RuntimeException("更新数据已失效");
}
List<LitemallGroupon> grouponList = grouponService.queryJoinRecord(groupon.getGrouponId());
if (groupon.getGrouponId() != 0 && (grouponList.size() >= grouponRules.getDiscountMember() - 1)) {
for (LitemallGroupon grouponActivity : grouponList) {
grouponActivity.setStatus(GrouponConstant.STATUS_SUCCEED);
grouponService.updateById(grouponActivity);
}
LitemallGroupon grouponSource = grouponService.queryById(groupon.getGrouponId());
grouponSource.setStatus(GrouponConstant.STATUS_SUCCEED);
grouponService.updateById(grouponSource);
}
}
//TODO 发送邮件和短信通知,这里采用异步发送
// 订单支付成功以后,会发送短信给用户,以及发送邮件给管理员View on GitHub (pinned to a1ef964a71)
Solutions
- Make the notify idempotent: at the top of the handler, look up the order by out_trade_no; if its pay status is already 'paid', return WxPayNotifyResponse.success immediately without touching groupon rows again.
- For a 0-row groupon update, log the orderId and respond success-with-warning (or fail so WeChat retries) instead of letting the RuntimeException abort the callback — decide per business rule whether a missing groupon row should block marking the order paid.
- If it reproduces deterministically, check litemall_groupon for that order id: deleted flag, duplicate rows, and whether the row's id on the entity matches an existing primary key.
Example fix
// before
groupon.setStatus(GrouponConstant.STATUS_ON);
if (grouponService.updateById(groupon) == 0) {
throw new RuntimeException("更新数据已失效");
}
// after - idempotent notify, no throw on stale row
LitemallOrder paidOrder = orderService.findBySn(orderSn);
if (paidOrder == null || !OrderUtil.isPayPending(paidOrder)) {
return WxPayNotifyResponse.success("订单已处理");
}
groupon.setStatus(GrouponConstant.STATUS_ON);
if (grouponService.updateById(groupon) == 0) {
logger.error("团购记录更新失效, grouponId={}, orderId={}", groupon.getId(), order.getId());
} Defensive patterns
Strategy: fallback
Validate before calling
// idempotency guard at the top of the notify handler
LitemallOrder order = orderService.findBySn(orderSn);
if (order == null) {
return WxPayNotifyResponse.fail("订单不存在");
}
if (!OrderUtil.isPayPending(order)) {
return WxPayNotifyResponse.success("订单已处理"); // duplicate notify: ack and exit
} Try / catch
try {
groupon.setStatus(GrouponConstant.STATUS_ON);
if (grouponService.updateById(groupon) == 0) {
logger.error("团购记录更新失效 grouponId={} orderId={}", groupon.getId(), order.getId());
// do not rethrow: ack-ordering decision belongs to business logic, not the callback
}
} catch (Exception e) {
logger.error("支付回调处理异常 orderId={}", order.getId(), e);
return WxPayNotifyResponse.fail(e.getMessage()); // WeChat will retry
} Prevention
- Make pay-notify idempotent on out_trade_no before touching any related tables.
- Never let an exception escape the notify endpoint uncaught — always answer with WxPayNotifyResponse so WeChat's retry schedule stays meaningful.
- Log orderId/grouponId on every 0-row update; stale groupon rows usually indicate a late payment on a dead groupon.
When it happens
Trigger: WeChat sends the pay-notify for a groupon order whose litemall_groupon row has since been deleted (logical delete flag set), or an admin/duplicate notify concurrently updated the same row so the second updateByPrimaryKey matches nothing. Duplicate notify retries from WeChat racing with the first callback are a classic trigger.
Common situations: WeChat retries the notify while the first callback is still in-flight (no idempotency guard on out_trade_no); the groupon/order was cancelled and logically deleted right before payment landed (late payment on an expired/cancelled groupon); manual DB cleanup of test groupon rows.
Related errors
AI-assisted analysis of linlinjava/litemall@a1ef964a71 (2026-08-14).
Data as JSON: /api/errors/2fc9fca1b82cb73c.
Report an issue: GitHub.