{"record":{"id":"3d7badd1baf12913","repo":"yudaocode/SpringBoot-Labs","slug":"error-3d7bad","errorCode":null,"errorMessage":"余额不足","messagePattern":"余额不足","errorType":"http","errorClass":"java.lang.Exception","httpStatus":500,"severity":"error","filePath":"labx-17/labx-17-sc-seata-at-feign-demo/labx-17-sc-seata-at-feign-demo-account-service/src/main/java/cn/iocoder/springcloud/labx17/accountservice/service/impl/AccountServiceImpl.java","lineNumber":34,"sourceCode":"\n    @Autowired\n    private AccountDao accountDao;\n\n    @Override\n    @Transactional // 开启新事物\n    public void reduceBalance(Long userId, Integer price) throws Exception {\n        logger.info(\"[reduceBalance] 当前 XID: {}\", RootContext.getXID());\n\n        // 检查余额\n        checkBalance(userId, price);\n\n        logger.info(\"[reduceBalance] 开始扣减用户 {} 余额\", userId);\n        // 扣除余额\n        int updateCount = accountDao.reduceBalance(price);\n        // 扣除成功\n        if (updateCount == 0) {\n            logger.warn(\"[reduceBalance] 扣除用户 {} 余额失败\", userId);\n            throw new Exception(\"余额不足\");\n        }\n        logger.info(\"[reduceBalance] 扣除用户 {} 余额成功\", userId);\n    }\n\n    private void checkBalance(Long userId, Integer price) throws Exception {\n        logger.info(\"[checkBalance] 检查用户 {} 余额\", userId);\n        Integer balance = accountDao.getBalance(userId);\n        if (balance < price) {\n            logger.warn(\"[checkBalance] 用户 {} 余额不足，当前余额:{}\", userId, balance);\n            throw new Exception(\"余额不足\");\n        }\n    }\n\n}\n","sourceCodeStart":16,"sourceCodeEnd":49,"githubUrl":"https://github.com/yudaocode/SpringBoot-Labs/blob/6c12efaed06d12907a0f40dd2ad1f7020aec8798/labx-17/labx-17-sc-seata-at-feign-demo/labx-17-sc-seata-at-feign-demo-account-service/src/main/java/cn/iocoder/springcloud/labx17/accountservice/service/impl/AccountServiceImpl.java#L16-L49","documentation":"Thrown by the account service of the Seata AT + OpenFeign demo (labx-17, Spring Cloud version). reduceBalance performs a conditional UPDATE that only decrements when the balance covers the price; 0 affected rows means the guard failed and the service throws a generic Exception to mark the branch as failed. Under Seata AT this exception propagates through the Feign call to the order service (TM), which marks the global transaction rollback-only so every branch's before-image is restored.","triggerScenarios":"Order flow invoked with order amount exceeding the user's balance such that checkBalance passed but the guarded UPDATE matched 0 rows (e.g. concurrent deductions), or directly calling account-service's reduceBalance with price > balance.","commonSituations":"Shared demo DB drained by earlier runs; two concurrent orders for the same user both passing the pre-check and the second getting 0 update count; amount/price unit mismatch between order and account services.","solutions":["Reseed/top up the account table balance for the test user.","Retry with a smaller order amount.","Map this exception at the order-service boundary to an 'insufficient balance' business response instead of surfacing a raw 500."],"exampleFix":"// before\nint updateCount = accountDao.reduceBalance(price);\nif (updateCount == 0) {\n    throw new Exception(\"余额不足\");\n}\n\n// after — pass userId into the guarded SQL and throw a domain exception\nint updateCount = accountDao.reduceBalance(userId, price); // WHERE user_id=? AND balance >= ?\nif (updateCount == 0) {\n    throw new InsufficientBalanceException(userId, price);\n}","handlingStrategy":"validation","validationCode":"// Order service, before starting the purchase\nInteger balance = accountFeign.getBalance(userId);\nif (balance == null || balance < totalPrice) {\n    return Result.error(\"余额不足\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    orderService.create(userId, productId, amount);\n} catch (Exception e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"余额不足\")) {\n        return ResponseEntity.badRequest().body(\"insufficient balance\");\n    }\n    log.error(\"global tx rolled back, XID={}\", RootContext.getXID(), e);\n    throw e;\n}","preventionTips":["Pre-validate balance via a read RPC before opening the global transaction.","Top up demo accounts before each run; reseed the DB.","Keep the SQL-level balance guard; treat 0-row updates as the definitive signal."],"tags":["seata","openfeign","distributed-transaction","business-rule","sql-update"],"backgroundTag":null,"analyzedSha":"6c12efaed06d12907a0f40dd2ad1f7020aec8798","analyzedAt":"2026-08-14T13:06:31.500Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}