环境:SpringBoot 2.5.15 + Java8 + MySQL8 + Redis + Redisson + RabbitMQ + 原生MyBatis XML包含:数据库全表、完整配置、全部实体、常量、工具类、Mapper+XML、Service、Controller、防重令牌、分布式锁、Redis 缓存、MQ 延时关单、乐观锁防超卖、短事务、支付 / 取消 / 超时关闭
一、SQL 全量建表语句
sql
CREATE DATABASE IF NOT EXISTS shop_full DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE shop_full;
-- 商品表(乐观锁version)
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
price DECIMAL(10,2) NOT NULL COMMENT '单价',
stock INT NOT NULL DEFAULT 0 COMMENT '库存',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 订单主表
CREATE TABLE order_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL COMMENT '订单号',
user_id BIGINT NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
pay_amount DECIMAL(10,2) DEFAULT 0,
order_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已取消 3已退款',
pay_time DATETIME NULL,
cancel_time DATETIME NULL,
expire_time DATETIME NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id),
KEY idx_order_status (order_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 订单明细表
CREATE TABLE order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
product_id BIGINT NOT NULL,
product_name VARCHAR(100) NOT NULL,
product_price DECIMAL(10,2) NOT NULL,
quantity INT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 支付记录表
CREATE TABLE pay_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
pay_no VARCHAR(64) COMMENT '第三方支付流水号',
pay_amount DECIMAL(10,2) NOT NULL,
pay_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1成功 2失败',
pay_type TINYINT DEFAULT 1 COMMENT '1微信 2支付宝',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 操作日志表
CREATE TABLE operation_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
content VARCHAR(255) NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 测试数据
INSERT INTO product(product_name,price,stock,version) VALUES ('华为手机',3999.00,100,0);
二、pom.xml 完整依赖
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.15</version>
<relativePath/>
</parent>
<groupId>com.ecommerce</groupId>
<artifactId>order-full-system</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.0</version>
</dependency>
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- 定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-task</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
三、application.yml 完整配置
yaml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/shop_full?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
password:
database: 0
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
task:
scheduling:
core-size: 5
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
# 业务配置
order:
expire-minute: 30
token-expire-minute: 5
# Redis Key前缀
redis:
key:
order-token: "order:token:"
product-stock: "product:stock:"
product-info: "product:info:"
四、全局常量类
OrderConstant.java
java
运行
public class OrderConstant {
// 订单状态
public static final int ORDER_WAIT_PAY = 0;
public static final int ORDER_PAY_SUCCESS = 1;
public static final int ORDER_CANCEL = 2;
public static final int ORDER_REFUND = 3;
// 支付状态
public static final int PAY_WAIT = 0;
public static final int PAY_SUCCESS = 1;
public static final int PAY_FAIL = 2;
}
MqConstant.java
java
运行
public class MqConstant {
public static final String ORDER_NORMAL_QUEUE = "order.normal.queue";
public static final String ORDER_NORMAL_EXCHANGE = "order.normal.exchange";
public static final String ORDER_NORMAL_ROUTING_KEY = "order.normal.key";
public static final String ORDER_DELAY_QUEUE = "order.delay.queue";
public static final String ORDER_DELAY_EXCHANGE = "order.delay.exchange";
public static final String ORDER_DELAY_ROUTING_KEY = "order.delay.key";
}
五、统一返回结果 Result.java
java
运行
import lombok.Data;
@Data
public class Result {
private Integer code;
private String msg;
private Object data;
public static Result success(Object data, String msg) {
Result result = new Result();
result.setCode(200);
result.setMsg(msg);
result.setData(data);
return result;
}
public static Result fail(String msg) {
Result result = new Result();
result.setCode(500);
result.setMsg(msg);
return result;
}
}
六、全部实体类
Product.java
java
运行
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class Product {
private Long id;
private String productName;
private BigDecimal price;
private Integer stock;
private Integer version;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
OrderInfo.java
java
运行
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class OrderInfo {
private Long id;
private String orderNo;
private Long userId;
private BigDecimal totalAmount;
private BigDecimal payAmount;
private Integer orderStatus;
private LocalDateTime payTime;
private LocalDateTime cancelTime;
private LocalDateTime expireTime;
private LocalDateTime createTime;
}
OrderItem.java
java
运行
import lombok.Data;
import java.math.BigDecimal;
@Data
public class OrderItem {
private Long id;
private String orderNo;
private Long productId;
private String productName;
private BigDecimal productPrice;
private Integer quantity;
}
PayLog.java
java
运行
import lombok.Data;
import java.math.BigDecimal;
@Data
public class PayLog {
private Long id;
private String orderNo;
private String payNo;
private BigDecimal payAmount;
private Integer payStatus;
private Integer payType;
}
OperationLog.java
java
运行
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class OperationLog {
private Long id;
private String content;
private LocalDateTime createTime;
}
七、工具类 & 配置类
RedisUtil.java
java
运行
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Service
public class RedisUtil {
@Resource
private RedisTemplate<String, String> stringRedisTemplate;
public void set(String key, String value, long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, value, time, unit);
}
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
public Boolean delete(String key) {
return stringRedisTemplate.delete(key);
}
public boolean checkAndDelete(String key) {
String script = "if redis.call('get',KEYS[1]) then return redis.call('del',KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key));
return result != null && result > 0;
}
}
RedissonConfig.java
java
运行
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class RedissonConfig {
@Resource
private RedisProperties redisProperties;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
String address = "redis://" + redisProperties.getHost() + ":" + redisProperties.getPort();
config.useSingleServer()
.setAddress(address)
.setPassword(redisProperties.getPassword())
.setDatabase(redisProperties.getDatabase());
return Redisson.create(config);
}
}
RabbitConfig.java
java
运行
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitConfig {
@Bean
public Queue orderNormalQueue() {
return new Queue(MqConstant.ORDER_NORMAL_QUEUE, true);
}
@Bean
public DirectExchange orderNormalExchange() {
return new DirectExchange(MqConstant.ORDER_NORMAL_EXCHANGE);
}
@Bean
public Binding orderNormalBinding() {
return BindingBuilder.bind(orderNormalQueue())
.to(orderNormalExchange())
.with(MqConstant.ORDER_NORMAL_ROUTING_KEY);
}
@Bean
public Queue orderDelayQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", MqConstant.ORDER_NORMAL_EXCHANGE);
args.put("x-dead-letter-routing-key", MqConstant.ORDER_NORMAL_ROUTING_KEY);
return new Queue(MqConstant.ORDER_DELAY_QUEUE, true, false, false, args);
}
@Bean
public DirectExchange orderDelayExchange() {
return new DirectExchange(MqConstant.ORDER_DELAY_EXCHANGE);
}
@Bean
public Binding orderDelayBinding() {
return BindingBuilder.bind(orderDelayQueue())
.to(orderDelayExchange())
.with(MqConstant.ORDER_DELAY_ROUTING_KEY);
}
}
八、Mapper 接口
ProductMapper.java
java
运行
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ProductMapper {
Product selectById(Long id);
int deductStock(@Param("id") Long id, @Param("num") Integer num, @Param("version") Integer version);
int addStock(@Param("id") Long id, @Param("num") Integer num);
}
OrderMapper.java
java
运行
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface OrderMapper {
void insert(OrderInfo orderInfo);
OrderInfo selectByOrderNo(String orderNo);
int updateOrderStatus(@Param("orderNo") String orderNo,
@Param("status") Integer status,
@Param("oldStatus") Integer oldStatus,
@Param("payTime") LocalDateTime payTime);
int updateOrderCancel(@Param("orderNo") String orderNo, @Param("cancelTime") LocalDateTime cancelTime);
List<String> listExpireWaitPayOrder();
}
OrderItemMapper.java
java
运行
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderItemMapper {
void insert(OrderItem orderItem);
OrderItem selectByOrderNo(String orderNo);
}
PayLogMapper.java
java
运行
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
@Mapper
public interface PayLogMapper {
void insert(PayLog payLog);
void updatePaySuccess(@Param("orderNo") String orderNo,
@Param("payNo") String payNo,
@Param("payType") Integer payType,
@Param("payAmount") BigDecimal payAmount);
}
OperationLogMapper.java
java
运行
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OperationLogMapper {
void insert(OperationLog log);
}
九、Mapper XML 文件
ProductMapper.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.ProductMapper">
<select id="selectById" resultType="com.ecommerce.entity.Product">
select * from product where id = #{id}
</select>
<update id="deductStock">
UPDATE product
SET stock = stock - #{num}, version = version + 1
WHERE id = #{id} AND version = #{version} AND stock >= #{num}
</update>
<update id="addStock">
UPDATE product SET stock = stock + #{num} WHERE id = #{id}
</update>
</mapper>
OrderMapper.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.OrderMapper">
<insert id="insert">
INSERT INTO order_info(order_no,user_id,total_amount,order_status,expire_time)
VALUES(#{orderNo},#{userId},#{totalAmount},#{orderStatus},#{expireTime})
</insert>
<select id="selectByOrderNo" resultType="com.ecommerce.entity.OrderInfo">
select * from order_info where order_no = #{orderNo}
</select>
<update id="updateOrderStatus">
UPDATE order_info SET order_status=#{status},pay_time=#{payTime}
WHERE order_no=#{orderNo} AND order_status=#{oldStatus}
</update>
<update id="updateOrderCancel">
UPDATE order_info SET order_status=2,cancel_time=#{cancelTime} WHERE order_no=#{orderNo}
</update>
<select id="listExpireWaitPayOrder" resultType="java.lang.String">
SELECT order_no FROM order_info
WHERE order_status = 0 AND expire_time < NOW() LIMIT 100
</select>
</mapper>
OrderItemMapper.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.OrderItemMapper">
<insert id="insert">
INSERT INTO order_item(order_no,product_id,product_name,product_price,quantity)
VALUES(#{orderNo},#{productId},#{productName},#{productPrice},#{quantity})
</insert>
<select id="selectByOrderNo" resultType="com.ecommerce.entity.OrderItem">
select * from order_item where order_no = #{orderNo}
</select>
</mapper>
PayLogMapper.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.PayLogMapper">
<insert id="insert">
INSERT INTO pay_log(order_no,pay_amount,pay_status)
VALUES(#{orderNo},#{payAmount},#{payStatus})
</insert>
<update id="updatePaySuccess">
UPDATE pay_log SET pay_no=#{payNo},pay_type=#{payType},pay_status=1 WHERE order_no=#{orderNo}
</update>
</mapper>
OperationLogMapper.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.OperationLogMapper">
<insert id="insert">
INSERT INTO operation_log(content) VALUES(#{content})
</insert>
</mapper>
十、Service 层全量代码
OperationLogService.java
java
运行
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class OperationLogService {
@Resource
private OperationLogMapper operationLogMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void recordLog(String content) {
OperationLog log = new OperationLog();
log.setContent(content);
operationLogMapper.insert(log);
}
}
OrderMqProducer.java
java
运行
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class OrderMqProducer {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendNormalMsg(String orderNo) {
rabbitTemplate.convertAndSend(MqConstant.ORDER_NORMAL_EXCHANGE,
MqConstant.ORDER_NORMAL_ROUTING_KEY,orderNo);
}
public void sendDelayOrderMsg(String orderNo, long delayMinute) {
long delay = delayMinute * 60 * 1000;
rabbitTemplate.convertAndSend(MqConstant.ORDER_DELAY_EXCHANGE,
MqConstant.ORDER_DELAY_ROUTING_KEY, orderNo, msg -> {
msg.getMessageProperties().setExpiration(String.valueOf(delay));
return msg;
});
}
}
OrderMqConsumer.java
java
运行
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service
public class OrderMqConsumer {
@Resource
private OrderService orderService;
@RabbitListener(queues = MqConstant.ORDER_NORMAL_QUEUE)
public void consume(String orderNo){
try {
orderService.autoCloseOrder(orderNo);
}catch (Exception e){
log.error("MQ关闭订单异常:{}",e.getMessage());
}
}
}
OrderService.java【核心】
java
运行
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class OrderService {
@Resource
private ProductMapper productMapper;
@Resource
private OrderMapper orderMapper;
@Resource
private OrderItemMapper orderItemMapper;
@Resource
private PayLogMapper payLogMapper;
@Resource
private OperationLogService operationLogService;
@Resource
private RedisUtil redisUtil;
@Resource
private RedissonClient redissonClient;
@Resource
private OrderMqProducer orderMqProducer;
@Value("${order.expire-minute}")
private Integer expireMinute;
@Value("${redis.key.order-token}")
private String orderTokenPrefix;
@Value("${redis.key.product-info}")
private String productInfoPrefix;
@Value("${redis.key.product-stock}")
private String productStockPrefix;
public String createOrder(Long userId, Long productId, Integer quantity, String orderToken) {
// 1. 防重令牌校验
String tokenKey = orderTokenPrefix + userId + ":" + orderToken;
if(!redisUtil.checkAndDelete(tokenKey)){
throw new RuntimeException("请勿重复提交");
}
// 2. 商品分布式锁
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
boolean lockOk = false;
try {
lockOk = lock.tryLock(0,30, TimeUnit.SECONDS);
if(!lockOk){
throw new RuntimeException("下单拥挤,请稍后重试");
}
// 3. Redis缓存查询
String infoKey = productInfoPrefix + productId;
String stockKey = productStockPrefix + productId;
Product product;
String stockStr = redisUtil.get(stockKey);
if(StringUtils.hasText(stockStr)){
product = JSON.parseObject(redisUtil.get(infoKey),Product.class);
if(Integer.parseInt(stockStr) < quantity){
throw new RuntimeException("库存不足");
}
}else{
product = productMapper.selectById(productId);
if(product == null){
throw new RuntimeException("商品不存在");
}
redisUtil.set(infoKey,JSON.toJSONString(product),1,TimeUnit.HOURS);
redisUtil.set(stockKey,product.getStock().toString(),1,TimeUnit.HOURS);
}
if(product.getStock() < quantity){
throw new RuntimeException("库存不足");
}
// 4. 短事务落库
return doCreateOrder(productId,quantity,userId,product);
}catch (Exception e){
throw new RuntimeException(e.getMessage());
}finally {
if(lockOk && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
@Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_COMMITTED)
public String doCreateOrder(Long productId, Integer quantity, Long userId, Product product){
// 乐观锁扣库存
int rows = productMapper.deductStock(productId,quantity,product.getVersion());
if(rows <= 0){
throw new RuntimeException("并发冲突,下单失败");
}
// 缓存库存扣减
redisUtil.getOperations().increment(productStockPrefix+productId,-quantity);
// 生成订单
String orderNo = "ORD" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0,6);
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(expireMinute);
OrderInfo order = new OrderInfo();
order.setOrderNo(orderNo);
order.setUserId(userId);
order.setTotalAmount(product.getPrice().multiply(new BigDecimal(quantity)));
order.setOrderStatus(OrderConstant.ORDER_WAIT_PAY);
order.setExpireTime(expireTime);
orderMapper.insert(order);
OrderItem item = new OrderItem();
item.setOrderNo(orderNo);
item.setProductId(productId);
item.setProductName(product.getProductName());
item.setProductPrice(product.getPrice());
item.setQuantity(quantity);
orderItemMapper.insert(item);
PayLog payLog = new PayLog();
payLog.setOrderNo(orderNo);
payLog.setPayAmount(order.getTotalAmount());
payLog.setPayStatus(OrderConstant.PAY_WAIT);
payLogMapper.insert(payLog);
// 发送延时关单MQ
orderMqProducer.sendDelayOrderMsg(orderNo,expireMinute);
orderMqProducer.sendNormalMsg(orderNo);
operationLogService.recordLog("用户"+userId+"创建订单:"+orderNo);
return orderNo;
}
@Transactional(rollbackFor = Exception.class)
public void payOrder(String orderNo,String payNo,Integer payType){
OrderInfo order = orderMapper.selectByOrderNo(orderNo);
if(order == null || !order.getOrderStatus().equals(OrderConstant.ORDER_WAIT_PAY)){
throw new RuntimeException("订单状态异常");
}
orderMapper.updateOrderStatus(orderNo,OrderConstant.ORDER_PAY_SUCCESS,
OrderConstant.ORDER_WAIT_PAY,LocalDateTime.now());
payLogMapper.updatePaySuccess(orderNo,payNo,payType,order.getTotalAmount());
operationLogService.recordLog("订单"+orderNo+"支付成功");
}
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(String orderNo){
OrderInfo order = orderMapper.selectByOrderNo(orderNo);
if(order == null || !order.getOrderStatus().equals(OrderConstant.ORDER_WAIT_PAY)){
throw new RuntimeException("无法取消");
}
OrderItem item = orderItemMapper.selectByOrderNo(orderNo);
productMapper.addStock(item.getProductId(),item.getQuantity());
orderMapper.updateOrderCancel(orderNo,LocalDateTime.now());
operationLogService.recordLog("订单"+orderNo+"手动取消");
}
@Transactional(rollbackFor = Exception.class)
public void autoCloseOrder(String orderNo){
OrderInfo order = orderMapper.selectByOrderNo(orderNo);
if(order == null || !order.getOrderStatus().equals(OrderConstant.ORDER_WAIT_PAY)){
return;
}
OrderItem item = orderItemMapper.selectByOrderNo(orderNo);
productMapper.addStock(item.getProductId(),item.getQuantity());
orderMapper.updateOrderCancel(orderNo,LocalDateTime.now());
operationLogService.recordLog("订单"+orderNo+"超时自动关闭");
}
}
十一、Controller 全量接口
OrderTokenController.java
java
运行
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/token")
public class OrderTokenController {
@Resource
private RedisUtil redisUtil;
@Value("${redis.key.order-token}")
private String orderTokenPrefix;
@Value("${order.token-expire-minute}")
private Integer tokenExpireMinute;
@GetMapping("/get")
public Result getToken(@RequestParam Long userId){
String token = UUID.randomUUID().toString().replace("-","");
String key = orderTokenPrefix + userId + ":" + token;
redisUtil.set(key,"1",tokenExpireMinute, TimeUnit.MINUTES);
return Result.success(token,"令牌获取成功");
}
}
OrderController.java
java
运行
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Resource
private OrderService orderService;
@PostMapping("/create")
public Result create(@RequestParam Long userId,
@RequestParam Long productId,
@RequestParam Integer quantity,
@RequestParam String orderToken){
try {
String orderNo = orderService.createOrder(userId,productId,quantity,orderToken);
return Result.success(orderNo,"下单成功");
}catch (Exception e){
return Result.fail(e.getMessage());
}
}
@PostMapping("/pay")
public Result pay(@RequestParam String orderNo,
@RequestParam String payNo,
@RequestParam Integer payType){
try {
orderService.payOrder(orderNo,payNo,payType);
return Result.success(null,"支付成功");
}catch (Exception e){
return Result.fail(e.getMessage());
}
}
@PostMapping("/cancel")
public Result cancel(@RequestParam String orderNo){
try {
orderService.cancelOrder(orderNo);
return Result.success(null,"取消成功");
}catch (Exception e){
return Result.fail(e.getMessage());
}
}
}
十二、启动类
java
运行
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
十三、测试流程
- 启动 MySQL、Redis、RabbitMQ
- 执行上方 SQL 建表 + 初始化商品
- 启动项目
- 接口调用顺序:
plaintext
1. 获取下单令牌:GET /api/token/get?userId=1 2. 创建订单:POST /api/order/create?userId=1&productId=1&quantity=1&orderToken=xxx 3. 模拟支付:POST /api/order/pay?orderNo=xxx&payNo=PAY666&payType=2 4. 手动取消:POST /api/order/cancel?orderNo=xxx
十四、核心面试总结(可直接背)
- 防重:Redis+Lua 令牌防重复提交
- 防超卖:Redisson 商品级分布式锁 + MySQL 乐观锁
- 高性能:Redis 热点缓存、短事务、异步 MQ 解耦
- 流量削峰:RabbitMQ 延时队列替代定时任务轮库
- 数据一致:下单扣库存、取消 / 超时自动回补库存
- 事务设计:核心 DB 操作为短事务,日志独立事务不回滚
1万+

被折叠的 条评论
为什么被折叠?



