diff --git a/src/main/java/com/laker/admin/module/ext/controller/ProductController.java b/src/main/java/com/laker/admin/module/ext/controller/ProductController.java new file mode 100644 index 0000000..c8b8459 --- /dev/null +++ b/src/main/java/com/laker/admin/module/ext/controller/ProductController.java @@ -0,0 +1,40 @@ +package com.laker.admin.module.ext.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.laker.admin.module.ext.service.ProductService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "△△△4.电商△△△") +@RequestMapping("/product") +@RestController +public class ProductController { + + @Autowired + private ProductService productService; + + /** + * 抢购商品 + * 运用工具(像 Postman 或者 Apache JMeter)模拟多个线程同时访问 /purchase/{productId} 接口。 + * 当多个线程同时抢购商品时,就可能出现超卖的情况。 + * + * @param productId 商品ID + * @return 抢购结果 + */ + @SaIgnore + @GetMapping("/purchase/{productId}") + @Operation(summary = "抢购商品") + public String purchase(@PathVariable Long productId) { + boolean success = productService.purchaseProduct(productId); + if (success) { + return "抢购成功"; + } else { + return "库存不足,抢购失败"; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/laker/admin/module/ext/entity/Product.java b/src/main/java/com/laker/admin/module/ext/entity/Product.java new file mode 100644 index 0000000..743ce5a --- /dev/null +++ b/src/main/java/com/laker/admin/module/ext/entity/Product.java @@ -0,0 +1,43 @@ +package com.laker.admin.module.ext.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +public class Product { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + private String name; + private int stock; + + public Product() { + } + + public Product(String name, int stock) { + this.name = name; + this.stock = stock; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getStock() { + return stock; + } + + public void setStock(int stock) { + this.stock = stock; + } +} \ No newline at end of file diff --git a/src/main/java/com/laker/admin/module/ext/mapper/ProductMapper.java b/src/main/java/com/laker/admin/module/ext/mapper/ProductMapper.java new file mode 100644 index 0000000..10ab02e --- /dev/null +++ b/src/main/java/com/laker/admin/module/ext/mapper/ProductMapper.java @@ -0,0 +1,11 @@ +package com.laker.admin.module.ext.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.laker.admin.module.ext.entity.Product; +import org.apache.ibatis.annotations.Update; + +public interface ProductMapper extends BaseMapper { + // 更新商品库存,防止超卖 + @Update("update product set stock = stock - 1 where id = #{productId} and stock > 0") + int purchaseProduct(Long productId); +} diff --git a/src/main/java/com/laker/admin/module/ext/service/ProductService.java b/src/main/java/com/laker/admin/module/ext/service/ProductService.java new file mode 100644 index 0000000..a14d999 --- /dev/null +++ b/src/main/java/com/laker/admin/module/ext/service/ProductService.java @@ -0,0 +1,36 @@ +package com.laker.admin.module.ext.service; + +import com.laker.admin.module.ext.entity.Product; +import com.laker.admin.module.ext.mapper.ProductMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class ProductService { + + @Autowired + private ProductMapper productMapper; + + // @Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED) + public boolean purchaseProduct(Long productId) { + log.info("开始购买商品,商品ID: {}", productId); + // 查询商品库存 + Product product = productMapper.selectById(productId); + // 如果商品不存在或者库存不足,返回失败 + if (product == null || product.getStock() <= 0) { + log.warn("商品不存在或者库存不足,商品ID: {}", productId); + return false; + } + // 更新商品库存,如果库存不足,更新失败 + final int updateCount = productMapper.purchaseProduct(productId); + if (updateCount > 0) { + log.info("购买成功,商品ID: {}", productId); + return true; + } else { + log.warn("购买失败,商品ID: {}", productId); + } + return false; + } +} \ No newline at end of file diff --git a/src/main/resources/db/migration/V2025.04.03__add_product.sql b/src/main/resources/db/migration/V2025.04.03__add_product.sql new file mode 100644 index 0000000..80918c9 --- /dev/null +++ b/src/main/resources/db/migration/V2025.04.03__add_product.sql @@ -0,0 +1,9 @@ +-- 商品表 +DROP TABLE IF EXISTS `product`; +CREATE TABLE `product` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(100) DEFAULT NULL, + `stock` bigint DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +INSERT INTO product (id, name, stock) VALUES(1, 'aa', 100); \ No newline at end of file diff --git a/src/test/java/com/laker/admin/example/ThreadCounter.java b/src/test/java/com/laker/admin/example/ThreadCounter.java new file mode 100644 index 0000000..78abe60 --- /dev/null +++ b/src/test/java/com/laker/admin/example/ThreadCounter.java @@ -0,0 +1,30 @@ +package com.laker.admin.example; + +import org.junit.jupiter.api.Test; + +public class ThreadCounter { + private int count = 0; + + @Test + public void testThreadCounter() throws InterruptedException { + Runnable task = new Runnable() { + public void run() { + for (int i = 0; i < 10000; ++i) { + count += 1; + } + } + }; + + Thread t1 = new Thread(task); + t1.start(); + + Thread t2 = new Thread(task); + t2.start(); + + t1.join(); + t2.join(); + +// Assertions.assertThat(count).isEqualTo(20000); + + } +} \ No newline at end of file