{"record":{"id":"2fc9fca1b82cb73c","repo":"linlinjava/litemall","slug":"error-2fc9fc","errorCode":null,"errorMessage":"更新数据已失效","messagePattern":"更新数据已失效","errorType":"http","errorClass":"RuntimeException","httpStatus":500,"severity":"critical","filePath":"litemall-wx-api/src/main/java/org/linlinjava/litemall/wx/service/WxOrderService.java","lineNumber":496,"sourceCode":"\n            LitemallOrder o = new LitemallOrder();\n            o.setId(orderId);\n            o.setOrderStatus(OrderUtil.STATUS_PAY);\n            orderService.updateSelective(o);\n\n            //  支付成功，有团购信息，更新团购信息\n            LitemallGroupon groupon = grouponService.queryByOrderId(order.getId());\n            if (groupon != null) {\n                grouponRules = grouponRulesService.findById(groupon.getRulesId());\n\n                //仅当发起者才创建分享图片\n                if (groupon.getGrouponId() == 0) {\n                    String url = qCodeService.createGrouponShareImage(grouponRules.getGoodsName(), grouponRules.getPicUrl(), groupon);\n                    groupon.setShareUrl(url);\n                }\n                groupon.setStatus(GrouponConstant.STATUS_ON);\n                if (grouponService.updateById(groupon) == 0) {\n                    throw new RuntimeException(\"更新数据已失效\");\n                }\n\n\n                List<LitemallGroupon> grouponList = grouponService.queryJoinRecord(groupon.getGrouponId());\n                if (groupon.getGrouponId() != 0 && (grouponList.size() >= grouponRules.getDiscountMember() - 1)) {\n                    for (LitemallGroupon grouponActivity : grouponList) {\n                        grouponActivity.setStatus(GrouponConstant.STATUS_SUCCEED);\n                        grouponService.updateById(grouponActivity);\n                    }\n\n                    LitemallGroupon grouponSource = grouponService.queryById(groupon.getGrouponId());\n                    grouponSource.setStatus(GrouponConstant.STATUS_SUCCEED);\n                    grouponService.updateById(grouponSource);\n                }\n            }\n\n            //TODO 发送邮件和短信通知，这里采用异步发送\n            // 订单支付成功以后，会发送短信给用户，以及发送邮件给管理员","sourceCodeStart":478,"sourceCodeEnd":514,"githubUrl":"https://github.com/linlinjava/litemall/blob/a1ef964a718b7277925b19ea26afe78ea3a1d325/litemall-wx-api/src/main/java/org/linlinjava/litemall/wx/service/WxOrderService.java#L478-L514","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ngroupon.setStatus(GrouponConstant.STATUS_ON);\nif (grouponService.updateById(groupon) == 0) {\n    throw new RuntimeException(\"更新数据已失效\");\n}\n\n// after - idempotent notify, no throw on stale row\nLitemallOrder paidOrder = orderService.findBySn(orderSn);\nif (paidOrder == null || !OrderUtil.isPayPending(paidOrder)) {\n    return WxPayNotifyResponse.success(\"订单已处理\");\n}\ngroupon.setStatus(GrouponConstant.STATUS_ON);\nif (grouponService.updateById(groupon) == 0) {\n    logger.error(\"团购记录更新失效, grouponId={}, orderId={}\", groupon.getId(), order.getId());\n}","handlingStrategy":"fallback","validationCode":"// idempotency guard at the top of the notify handler\nLitemallOrder order = orderService.findBySn(orderSn);\nif (order == null) {\n    return WxPayNotifyResponse.fail(\"订单不存在\");\n}\nif (!OrderUtil.isPayPending(order)) {\n    return WxPayNotifyResponse.success(\"订单已处理\"); // duplicate notify: ack and exit\n}","typeGuard":null,"tryCatchPattern":"try {\n    groupon.setStatus(GrouponConstant.STATUS_ON);\n    if (grouponService.updateById(groupon) == 0) {\n        logger.error(\"团购记录更新失效 grouponId={} orderId={}\", groupon.getId(), order.getId());\n        // do not rethrow: ack-ordering decision belongs to business logic, not the callback\n    }\n} catch (Exception e) {\n    logger.error(\"支付回调处理异常 orderId={}\", order.getId(), e);\n    return WxPayNotifyResponse.fail(e.getMessage()); // WeChat will retry\n}","preventionTips":["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."],"tags":["payment","wechat-pay","groupon","idempotency","concurrency","litemall"],"backgroundTag":null,"analyzedSha":"a1ef964a718b7277925b19ea26afe78ea3a1d325","analyzedAt":"2026-08-14T12:39:46.078Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}