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

  1. 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.
  2. 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.
  3. 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

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.