fix(seata): 修复seata示例项目余额不足时没有正确回滚库存的问题 fix #9287

This commit is contained in:
牧星长zolay 2026-04-15 18:06:55 +08:00 committed by GitHub
parent cf7eeac6ab
commit 03ee0616a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 55 additions and 25 deletions

View File

@ -1,6 +1,7 @@
package org.jeecg.modules.test.seata.account.controller; package org.jeecg.modules.test.seata.account.controller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.test.seata.account.service.SeataAccountService; import org.jeecg.modules.test.seata.account.service.SeataAccountService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -20,7 +21,7 @@ public class SeataAccountController {
private SeataAccountService accountService; private SeataAccountService accountService;
@PostMapping("/reduceBalance") @PostMapping("/reduceBalance")
public void reduceBalance(Long userId, BigDecimal amount) { public Result<?> reduceBalance(Long userId, BigDecimal amount) {
accountService.reduceBalance(userId, amount); return accountService.reduceBalance(userId, amount);
} }
} }

View File

@ -1,5 +1,7 @@
package org.jeecg.modules.test.seata.account.service; package org.jeecg.modules.test.seata.account.service;
import org.jeecg.common.api.vo.Result;
import java.math.BigDecimal; import java.math.BigDecimal;
/** /**
@ -14,5 +16,5 @@ public interface SeataAccountService {
* @param userId 用户 ID * @param userId 用户 ID
* @param amount 扣减金额 * @param amount 扣减金额
*/ */
void reduceBalance(Long userId, BigDecimal amount); Result<?> reduceBalance(Long userId, BigDecimal amount);
} }

View File

@ -5,6 +5,7 @@ import com.baomidou.dynamic.datasource.annotation.DS;
import io.seata.core.context.RootContext; import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.test.seata.account.entity.SeataAccount; import org.jeecg.modules.test.seata.account.entity.SeataAccount;
import org.jeecg.modules.test.seata.account.mapper.SeataAccountMapper; import org.jeecg.modules.test.seata.account.mapper.SeataAccountMapper;
import org.jeecg.modules.test.seata.account.service.SeataAccountService; import org.jeecg.modules.test.seata.account.service.SeataAccountService;
@ -34,7 +35,7 @@ public class SeataAccountServiceImpl implements SeataAccountService {
@DS("account") @DS("account")
@Override @Override
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class) @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void reduceBalance(Long userId, BigDecimal amount) { public Result<?> reduceBalance(Long userId, BigDecimal amount) {
log.info("xid:"+ RootContext.getXID()); log.info("xid:"+ RootContext.getXID());
log.info("=============ACCOUNT START================="); log.info("=============ACCOUNT START=================");
SeataAccount account = accountMapper.selectById(userId); SeataAccount account = accountMapper.selectById(userId);
@ -44,7 +45,7 @@ public class SeataAccountServiceImpl implements SeataAccountService {
if (balance.compareTo(amount)==-1) { if (balance.compareTo(amount)==-1) {
log.warn("用户 {} 余额不足,当前余额:{}", userId, balance); log.warn("用户 {} 余额不足,当前余额:{}", userId, balance);
throw new RuntimeException("余额不足"); return Result.error("余额不足");
} }
log.info("开始扣减用户 {} 余额", userId); log.info("开始扣减用户 {} 余额", userId);
BigDecimal currentBalance = account.getBalance().subtract(amount); BigDecimal currentBalance = account.getBalance().subtract(amount);
@ -52,5 +53,6 @@ public class SeataAccountServiceImpl implements SeataAccountService {
accountMapper.updateById(account); accountMapper.updateById(account);
log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance); log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance);
log.info("=============ACCOUNT END================="); log.info("=============ACCOUNT END=================");
return Result.OK();
} }
} }

View File

@ -4,7 +4,7 @@ spring:
data: data:
redis: redis:
##redis 单机环境配置 ##redis 单机环境配置
host: localhost host: jeecg-boot-redis
port: 6379 port: 6379
database: 0 database: 0
password: password:
@ -22,7 +22,7 @@ spring:
autoconfigure: autoconfigure:
exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
datasource: datasource:
url: jdbc:mysql://127.0.0.1:3306/jeecg_account?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg_account?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root username: root
password: root password: root
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
@ -30,7 +30,7 @@ spring:
init: init:
schema-locations: classpath:sql/schema-account.sql schema-locations: classpath:sql/schema-account.sql
seata: seata:
enable-auto-data-source-proxy: false enable-auto-data-source-proxy: true
service: service:
grouplist: grouplist:
default: 127.0.0.1:8091 default: 127.0.0.1:8091

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.test.seata.order.feign; package org.jeecg.modules.test.seata.order.feign;
import org.jeecg.common.api.vo.Result;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -19,5 +20,5 @@ public interface AccountClient {
* @return * @return
*/ */
@PostMapping("/test/seata/account/reduceBalance") @PostMapping("/test/seata/account/reduceBalance")
String reduceBalance(@RequestParam("userId") Long userId, @RequestParam("amount") BigDecimal amount); Result<?> reduceBalance(@RequestParam("userId") Long userId, @RequestParam("amount") BigDecimal amount);
} }

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.test.seata.order.feign; package org.jeecg.modules.test.seata.order.feign;
import org.jeecg.common.api.vo.Result;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -21,5 +22,5 @@ public interface ProductClient {
* @return * @return
*/ */
@PostMapping("/test/seata/product/reduceStock") @PostMapping("/test/seata/product/reduceStock")
BigDecimal reduceStock(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); Result<BigDecimal> reduceStock(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
} }

View File

@ -5,6 +5,9 @@ import com.baomidou.dynamic.datasource.annotation.DS;
import io.seata.core.context.RootContext; import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional; import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.test.seata.order.dto.PlaceOrderRequest; import org.jeecg.modules.test.seata.order.dto.PlaceOrderRequest;
import org.jeecg.modules.test.seata.order.entity.SeataOrder; import org.jeecg.modules.test.seata.order.entity.SeataOrder;
import org.jeecg.modules.test.seata.order.enums.OrderStatus; import org.jeecg.modules.test.seata.order.enums.OrderStatus;
@ -59,13 +62,20 @@ public class SeataOrderServiceImpl implements SeataOrderService {
orderMapper.insert(order); orderMapper.insert(order);
log.info("订单一阶段生成,等待扣库存付款中"); log.info("订单一阶段生成,等待扣库存付款中");
// 扣减库存并计算总价 // 扣减库存并计算总价
BigDecimal amount = productClient.reduceStock(productId, count); Result<BigDecimal> productRes = productClient.reduceStock(productId, count);
if (!productRes.isSuccess()) {
String message = productRes.getMessage();
message = oConvertUtils.isEmpty(message) ? "操作失败" : message;
throw new JeecgBootBizTipException(message);
}
BigDecimal amount = productRes.getResult();
// 扣减余额 // 扣减余额
String str = accountClient.reduceBalance(userId, amount); Result<?> accountRes = accountClient.reduceBalance(userId, amount);
// feign响应被二次封装判断使主事务回滚 // feign响应被二次封装判断使主事务回滚
JSONObject jsonObject = JSONObject.parseObject(str); if (!accountRes.isSuccess()) {
if (jsonObject.getInteger("code") != 200) { String message = accountRes.getMessage();
throw new RuntimeException(); message = oConvertUtils.isEmpty(message) ? "操作失败" : message;
throw new JeecgBootBizTipException(message);
} }
order.setStatus(OrderStatus.SUCCESS); order.setStatus(OrderStatus.SUCCESS);

View File

@ -4,7 +4,7 @@ spring:
data: data:
redis: redis:
##redis 单机环境配置 ##redis 单机环境配置
host: localhost host: jeecg-boot-redis
port: 6379 port: 6379
database: 0 database: 0
password: password:
@ -23,14 +23,14 @@ spring:
exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
datasource: datasource:
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jeecg_order?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg_order?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root username: root
password: root password: root
sql: sql:
init: init:
schema-locations: classpath:sql/schema-order.sql schema-locations: classpath:sql/schema-order.sql
seata: seata:
enable-auto-data-source-proxy: false enable-auto-data-source-proxy: true
service: service:
grouplist: grouplist:
default: 127.0.0.1:8091 default: 127.0.0.1:8091

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.test.seata.product.controller; package org.jeecg.modules.test.seata.product.controller;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.test.seata.product.service.SeataProductService; import org.jeecg.modules.test.seata.product.service.SeataProductService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -20,7 +21,7 @@ public class SeataProductController {
private SeataProductService seataProductService; private SeataProductService seataProductService;
@PostMapping("/reduceStock") @PostMapping("/reduceStock")
public BigDecimal reduceStock(Long productId, Integer count, HttpServletRequest request) { public Result<BigDecimal> reduceStock(Long productId, Integer count, HttpServletRequest request) {
return seataProductService.reduceStock(productId, count); return seataProductService.reduceStock(productId, count);
} }
} }

View File

@ -1,5 +1,7 @@
package org.jeecg.modules.test.seata.product.service; package org.jeecg.modules.test.seata.product.service;
import org.jeecg.common.api.vo.Result;
import java.math.BigDecimal; import java.math.BigDecimal;
/** /**
@ -16,5 +18,5 @@ public interface SeataProductService {
* @param count 扣减数量 * @param count 扣减数量
* @return 商品总价 * @return 商品总价
*/ */
BigDecimal reduceStock(Long productId, Integer count); Result<BigDecimal> reduceStock(Long productId, Integer count);
} }

View File

@ -5,6 +5,7 @@ import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.test.seata.product.entity.SeataProduct; import org.jeecg.modules.test.seata.product.entity.SeataProduct;
import org.jeecg.modules.test.seata.product.mapper.SeataProductMapper; import org.jeecg.modules.test.seata.product.mapper.SeataProductMapper;
import org.jeecg.modules.test.seata.product.service.SeataProductService; import org.jeecg.modules.test.seata.product.service.SeataProductService;
@ -35,7 +36,7 @@ public class SeataProductServiceImpl implements SeataProductService {
@DS("product") @DS("product")
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class) @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
@Override @Override
public BigDecimal reduceStock(Long productId, Integer count) { public Result<BigDecimal> reduceStock(Long productId, Integer count) {
log.info("xid:"+ RootContext.getXID()); log.info("xid:"+ RootContext.getXID());
log.info("=============PRODUCT START================="); log.info("=============PRODUCT START=================");
// 检查库存 // 检查库存
@ -46,7 +47,7 @@ public class SeataProductServiceImpl implements SeataProductService {
if (stock < count) { if (stock < count) {
log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock); log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock);
throw new RuntimeException("库存不足"); return Result.error("库存不足");
} }
log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice()); log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice());
// 扣减库存 // 扣减库存
@ -56,6 +57,6 @@ public class SeataProductServiceImpl implements SeataProductService {
BigDecimal totalPrice = product.getPrice().multiply(new BigDecimal(count)); BigDecimal totalPrice = product.getPrice().multiply(new BigDecimal(count));
log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, count, totalPrice); log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, count, totalPrice);
log.info("=============PRODUCT END================="); log.info("=============PRODUCT END=================");
return totalPrice; return Result.OK(totalPrice);
} }
} }

View File

@ -1,6 +1,15 @@
server: server:
port: 5003 port: 5003
spring: spring:
data:
redis:
##redis 单机环境配置
host: jeecg-boot-redis
port: 6379
database: 0
password:
ssl:
enabled: false
application: application:
name: seata-product name: seata-product
cloud: cloud:
@ -14,14 +23,14 @@ spring:
exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
datasource: datasource:
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jeecg_product?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg_product?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root username: root
password: root password: root
sql: sql:
init: init:
schema-locations: classpath:sql/schema-product.sql schema-locations: classpath:sql/schema-product.sql
seata: seata:
enable-auto-data-source-proxy: false enable-auto-data-source-proxy: true
service: service:
grouplist: grouplist:
default: 127.0.0.1:8091 default: 127.0.0.1:8091