diff --git a/api/src/main/java/me/xiaoyan/point/api/controller/ShopGoodsController.java b/api/src/main/java/me/xiaoyan/point/api/controller/ShopGoodsController.java new file mode 100644 index 0000000..1dfb0e1 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/controller/ShopGoodsController.java @@ -0,0 +1,28 @@ +package me.xiaoyan.point.api.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import me.xiaoyan.point.api.pojo.Goods; +import me.xiaoyan.point.api.pojo.vo.PageParam; +import me.xiaoyan.point.api.service.GoodsService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("shop/goods") +public class ShopGoodsController { + @Resource + private GoodsService goodsService; + + @GetMapping("query") + public Page query( + int category, + int page, + int pageSize + ) { + return goodsService.queryByPage(category, Page.of(page, pageSize)); + } +} diff --git a/api/src/main/java/me/xiaoyan/point/api/controller/ShopOrderInfoController.java b/api/src/main/java/me/xiaoyan/point/api/controller/ShopOrderInfoController.java new file mode 100644 index 0000000..32224d7 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/controller/ShopOrderInfoController.java @@ -0,0 +1,76 @@ +package me.xiaoyan.point.api.controller; + +import cn.dev33.satoken.stp.StpUtil; +import lombok.extern.slf4j.Slf4j; +import me.xiaoyan.point.api.error.BizException; +import me.xiaoyan.point.api.pojo.OrderInfo; +import me.xiaoyan.point.api.pojo.vo.CreateOrderData; +import me.xiaoyan.point.api.service.GoodsService; +import me.xiaoyan.point.api.service.OrderInfoService; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 订单表(OrderInfo)表控制层 + * + * @author makejava + * @since 2022-11-24 09:32:38 + */ +@Slf4j +@RestController +@RequestMapping("shop/order") +public class ShopOrderInfoController { + @Resource + private OrderInfoService orderInfoService; + @Resource + private GoodsService goodsService; + @Resource + private StringRedisTemplate stringRedisTemplate; + + // 是否已经没有库存 + private ConcurrentHashMap stockOutMap = new ConcurrentHashMap<>(); + private static final String CACHE_STOCK_KEY = "goods:stock:"; + + private String cacheKey(long gid) { + return CACHE_STOCK_KEY + gid; + } + + @PostConstruct // 初始化执行一次 + private void initGoodsStockCache() { + //TODO 应该定时更新缓存数据 + goodsService.queryAllGoodsIdAndStock().forEach(g -> { + log.info("缓存 id:{} stock:{} ", g.getId(), g.getStock()); + // 缓存库存 + stringRedisTemplate.opsForValue().set(cacheKey(g.getId()), g.getStock().toString()); + }); + } + + @PostMapping("create") + public OrderInfo create(@Validated @RequestBody CreateOrderData data) { + if (data.getGoodsId() <= 0 || data.getBuyCount() <= 0) { + throw BizException.create("订单参数不正确"); + } + //1.内存判断 + if (stockOutMap.get(data.getGoodsId())) { + throw BizException.create("库存不足"); + } + //2.缓存(redis)判断 + long count = stringRedisTemplate.opsForValue().decrement(cacheKey(data.getGoodsId())); + if (count < 0) { + // 此时库存没有了 , 保存到已买完的对象 + stockOutMap.put(data.getGoodsId(),true); + log.info("stock count ===>" + count); + // 对缓存进行库存 + 1 + stringRedisTemplate.opsForValue().increment(cacheKey(data.getGoodsId())); // + throw BizException.create("库存不足"); + } + //3.数据库 + return orderInfoService.create(StpUtil.getLoginIdAsInt(), data); + } +} + diff --git a/api/src/main/java/me/xiaoyan/point/api/mapper/GoodsMapper.java b/api/src/main/java/me/xiaoyan/point/api/mapper/GoodsMapper.java new file mode 100644 index 0000000..00dee12 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/mapper/GoodsMapper.java @@ -0,0 +1,30 @@ +package me.xiaoyan.point.api.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import me.xiaoyan.point.api.pojo.Goods; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Entity me.xiaoyan.point.api.pojo.Goods + */ +@Mapper +public interface GoodsMapper extends BaseMapper { + public Page queryByCategory(@Param("category") int category,Page page); + + /** + * 扣除商品库存 + * @param id + * @param count + * @return + */ + public int deductCount(@Param("id") int id, @Param("count") int count); + public List queryAllGoodsIdAndStock(); +} + + + + diff --git a/api/src/main/java/me/xiaoyan/point/api/mapper/OrderInfoMapper.java b/api/src/main/java/me/xiaoyan/point/api/mapper/OrderInfoMapper.java new file mode 100644 index 0000000..88d36a9 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/mapper/OrderInfoMapper.java @@ -0,0 +1,17 @@ +package me.xiaoyan.point.api.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.xiaoyan.point.api.pojo.OrderInfo; +import org.apache.ibatis.annotations.Mapper; + +/** + * 订单表(OrderInfo)表数据库访问层 + * + * @author makejava + * @since 2022-11-24 09:32:38 + */ +@Mapper +public interface OrderInfoMapper extends BaseMapper { + +} + diff --git a/api/src/main/java/me/xiaoyan/point/api/pojo/Goods.java b/api/src/main/java/me/xiaoyan/point/api/pojo/Goods.java new file mode 100644 index 0000000..b2b40f7 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/pojo/Goods.java @@ -0,0 +1,107 @@ +package me.xiaoyan.point.api.pojo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +/** + * 商品表 + * @TableName goods + */ +@Data +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Builder +@TableName(value ="goods") +public class Goods implements Serializable { + /** + * + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 商品类别(1:普通 2:精选 3:秒杀 4:抽奖) + */ + private Integer category; + + /** + * 商品类型(1:实物 2:虚拟) + */ + private Integer type; + + /** + * + */ + private String title; + + /** + * 原价 + */ + private Integer originPrice; + + /** + * 价格 + */ + private Integer price; + + /** + * 库存数量 + */ + private Integer stock; + + /** + * 购买最大数量(0表示不限制) + */ + private Integer limitCount; + + /** + * 商品图 + */ + private String cover; + + /** + * 描述 + */ + private String description; + + /** + * 提示 + */ + private String notice; + + /** + * 上架时间 + */ + private Date onlineTime; + + /** + * 下架时间 + */ + private Date offlineTime; + + /** + * + */ + private Date createTime; + + /** + * + */ + private Date updateTime; + + /** + * + */ + private Integer status; +} \ No newline at end of file diff --git a/api/src/main/java/me/xiaoyan/point/api/pojo/OrderInfo.java b/api/src/main/java/me/xiaoyan/point/api/pojo/OrderInfo.java new file mode 100644 index 0000000..1da5846 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/pojo/OrderInfo.java @@ -0,0 +1,50 @@ +package me.xiaoyan.point.api.pojo; + +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 订单表(OrderInfo)表实体类 + * + * @author makejava + * @since 2022-11-24 09:32:42 + */ +@Data +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Builder +@TableName(value ="order_info") +public class OrderInfo { + @TableId + //订单编号 + private String id; + //商品编号 + private Long gid; + //价格 + private Integer price; + //购买数量 + private Integer count; + //用户编号 + private Integer uid; + //订单数据 + private String data; + + private Date createTime; + + private Date updateTime; + //订单状态(0:已删除 1:已取消 2:待确认 3:已完成) + private Integer status; + +} + diff --git a/api/src/main/java/me/xiaoyan/point/api/pojo/dto/OrderStatus.java b/api/src/main/java/me/xiaoyan/point/api/pojo/dto/OrderStatus.java new file mode 100644 index 0000000..c3656ec --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/pojo/dto/OrderStatus.java @@ -0,0 +1,8 @@ +package me.xiaoyan.point.api.pojo.dto; + +public class OrderStatus { + public static final int DELETE = 0; + public static final int CANCEL = 1; + public static final int CONFIRM = 2; + public static final int DONE = 3; +} diff --git a/api/src/main/java/me/xiaoyan/point/api/pojo/vo/CreateOrderData.java b/api/src/main/java/me/xiaoyan/point/api/pojo/vo/CreateOrderData.java new file mode 100644 index 0000000..0b3bfd5 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/pojo/vo/CreateOrderData.java @@ -0,0 +1,11 @@ +package me.xiaoyan.point.api.pojo.vo; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class CreateOrderData implements Serializable { + private int goodsId; + private int buyCount; +} diff --git a/api/src/main/java/me/xiaoyan/point/api/service/GoodsService.java b/api/src/main/java/me/xiaoyan/point/api/service/GoodsService.java new file mode 100644 index 0000000..ec61724 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/service/GoodsService.java @@ -0,0 +1,26 @@ +package me.xiaoyan.point.api.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import me.xiaoyan.point.api.pojo.Goods; +import com.baomidou.mybatisplus.extension.service.IService; +import me.xiaoyan.point.api.pojo.vo.PageParam; + +import java.util.List; + +/** + * + */ +public interface GoodsService extends IService { + + Page queryByPage(int category, Page page); + + /** + * 减库存 + * @param id + * @param count + * @return + */ + boolean deductStock(int id,int count); + + List queryAllGoodsIdAndStock(); +} diff --git a/api/src/main/java/me/xiaoyan/point/api/service/OrderInfoService.java b/api/src/main/java/me/xiaoyan/point/api/service/OrderInfoService.java new file mode 100644 index 0000000..bfa072a --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/service/OrderInfoService.java @@ -0,0 +1,17 @@ +package me.xiaoyan.point.api.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import me.xiaoyan.point.api.pojo.OrderInfo; +import me.xiaoyan.point.api.pojo.vo.CreateOrderData; + +/** + * 订单表(OrderInfo)表服务接口 + * + * @author makejava + * @since 2022-11-24 09:32:44 + */ +public interface OrderInfoService extends IService { + + OrderInfo create(int uid, CreateOrderData data); +} + diff --git a/api/src/main/java/me/xiaoyan/point/api/service/impl/GoodsServiceImpl.java b/api/src/main/java/me/xiaoyan/point/api/service/impl/GoodsServiceImpl.java new file mode 100644 index 0000000..4e0fa91 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/service/impl/GoodsServiceImpl.java @@ -0,0 +1,37 @@ +package me.xiaoyan.point.api.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import me.xiaoyan.point.api.pojo.Goods; +import me.xiaoyan.point.api.service.GoodsService; +import me.xiaoyan.point.api.mapper.GoodsMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * + */ +@Service +public class GoodsServiceImpl extends ServiceImpl + implements GoodsService { + + @Override + public Page queryByPage(int category, Page page) { + return this.getBaseMapper().queryByCategory(category, page); + } + + @Override + public boolean deductStock(int id, int count) { + return getBaseMapper().deductCount(id,count) == 1; + } + + @Override + public List queryAllGoodsIdAndStock() { + return getBaseMapper().queryAllGoodsIdAndStock(); + } +} + + + + diff --git a/api/src/main/java/me/xiaoyan/point/api/service/impl/OrderInfoServiceImpl.java b/api/src/main/java/me/xiaoyan/point/api/service/impl/OrderInfoServiceImpl.java new file mode 100644 index 0000000..d6183ce --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/service/impl/OrderInfoServiceImpl.java @@ -0,0 +1,84 @@ +package me.xiaoyan.point.api.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import me.xiaoyan.point.api.error.BizException; +import me.xiaoyan.point.api.mapper.OrderInfoMapper; +import me.xiaoyan.point.api.pojo.Goods; +import me.xiaoyan.point.api.pojo.OrderInfo; +import me.xiaoyan.point.api.pojo.UserInfo; +import me.xiaoyan.point.api.pojo.dto.OrderStatus; +import me.xiaoyan.point.api.pojo.vo.CreateOrderData; +import me.xiaoyan.point.api.service.GoodsService; +import me.xiaoyan.point.api.service.OrderInfoService; +import me.xiaoyan.point.api.service.UserInfoService; +import me.xiaoyan.point.api.util.OrderIdGenerator; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Date; + +/** + * 订单表(OrderInfo)表服务实现类 + * + * @author makejava + * @since 2022-11-24 09:32:44 + */ +@Service +public class OrderInfoServiceImpl extends ServiceImpl implements OrderInfoService { + + @Resource + private UserInfoService userInfoService; + @Resource + private GoodsService goodsService; + + @Transactional + @Override + public OrderInfo create(int uid, CreateOrderData data) { + // 1.查询用户信息 + final UserInfo user = userInfoService.getInfoById(uid); + // 判断用户信息 + if (user == null) throw BizException.create("用户信息不存在"); + if (user.getStatus() != 1) throw BizException.create("用户状态不正确"); + // 2.查询商品信息 + final Goods goods = goodsService.getById(data.getGoodsId()); + if (goods == null) throw BizException.create("兑换的商品不存在"); + long now = new Date().getTime(); + if (goods.getOnlineTime().getTime() > now || goods.getOfflineTime().getTime() < now) { + throw BizException.create("商品未上架或已下架"); + } + if (goods.getStock() < data.getBuyCount()) throw BizException.create("商品存库不足"); + // 判断购买数量的限制 + if(buyHistoryCount(uid,data.getGoodsId()) + data.getBuyCount() > goods.getLimitCount()){ + throw BizException.create("最多兑换" + goods.getLimitCount() + "件"); + } + // 3.减库存 + if (!goodsService.deductStock(data.getGoodsId(), data.getBuyCount())) { + throw BizException.create("商品库存不足"); + } + // 4.创建订单 + OrderInfo orderInfo = OrderInfo.builder() + .id(OrderIdGenerator.next()) + .gid((long) data.getGoodsId()) + .uid(uid) + .count(data.getBuyCount()) + .price(goods.getPrice()) + .status(OrderStatus.CONFIRM) + .build(); + if (save(orderInfo)) { + return orderInfo; + } + throw BizException.create("创建订单失败"); + } + + public long buyHistoryCount(int uid, int gid) { + QueryWrapper q = new QueryWrapper(); + q.eq("uid", gid); + q.eq("gid", gid); + q.ge("status", OrderStatus.CONFIRM); // 状态为2(待确认)和3(已完成) + return count(q); + } +} + diff --git a/api/src/main/java/me/xiaoyan/point/api/util/OrderIdGenerator.java b/api/src/main/java/me/xiaoyan/point/api/util/OrderIdGenerator.java new file mode 100644 index 0000000..7609e73 --- /dev/null +++ b/api/src/main/java/me/xiaoyan/point/api/util/OrderIdGenerator.java @@ -0,0 +1,28 @@ +package me.xiaoyan.point.api.util; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class OrderIdGenerator { + private static AtomicInteger atomic = new AtomicInteger(); + private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddHHmmss"); + private static long prevTime = 0; + + public static String next() { + long now = System.currentTimeMillis() / 1000; + if (now > prevTime) { + prevTime = now; + atomic.set(0); + } + return dateFormat.format(new Date()) + String.format("%05d", atomic.addAndGet(1)); + } + + public static void main(String[] args) throws InterruptedException { + for (int i = 0; i < 1000; i++) { + System.out.println(next()); + Thread.sleep(1); + } + } +} diff --git a/api/src/main/resources/db.sql b/api/src/main/resources/db.sql index b6d9c3c..496330c 100644 --- a/api/src/main/resources/db.sql +++ b/api/src/main/resources/db.sql @@ -31,7 +31,7 @@ create table point_record point int(10) not null, current_total_point int(10) not null, reason varchar(100) not null, - valid_time datetime default current_timestamp, + valid_time datetime default current_timestamp, expire_time datetime null ) engine = innodb comment '积分记录表'; @@ -41,10 +41,38 @@ create table sign_record uid int(10) not null, point int(10) not null, ip varchar(50) not null, - create_time datetime default current_timestamp + create_time datetime default current_timestamp ) engine = innodb comment '打卡记录表'; create table goods ( + id bigint(15) primary key auto_increment, + category tinyint(2) null default 1 comment '商品类别(1:普通 2:精选 3:秒杀 4:抽奖)', + type tinyint(2) null default 1 comment '商品类型(1:实物 2:虚拟)', + title varchar(50) not null, + origin_price int(10) unsigned comment '原价' default 0, + price int(10) unsigned not null comment '价格', + stock int(10) unsigned not null comment '库存数量', + limit_count int(10) unsigned null default 1 comment '购买最大数量(0表示不限制)', + cover varchar(200) not null comment '商品图', + description text not null comment '描述', + notice varchar(500) null comment '提示', + online_time datetime not null comment '上架时间', + offline_time datetime not null comment '下架时间', + create_time datetime default current_timestamp, + update_time datetime null on update current_timestamp, + status tinyint(2) default 1, + index ix_title (title) +) engine = innodb comment '商品表'; -) engine = innodb comment '商品表'; \ No newline at end of file +create table order_info +( + id varchar(50) not null comment '订单编号', + gid bigint(15) not null comment '商品编号', + price int(10) not null comment '价格', + uid int(10) not null comment '用户编号', + data json null comment '订单数据', + create_time datetime default current_timestamp, + update_time datetime null on update current_timestamp, + status tinyint(2) default 1 comment '订单状态(0:已删除 1:待确认 2:已取消 3:已完成)' +) comment '订单表'; \ No newline at end of file diff --git a/api/src/main/resources/docs/process/create_order.svg b/api/src/main/resources/docs/process/create_order.svg new file mode 100644 index 0000000..480d239 --- /dev/null +++ b/api/src/main/resources/docs/process/create_order.svg @@ -0,0 +1,4 @@ + + + +
创建订单
创建订单
正常
正常
用户状态是否正常
用户状态是否正常
查询商品信息
查询商品信息
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/api/src/main/resources/docs/process/创建订单.svg b/api/src/main/resources/docs/process/创建订单.svg new file mode 100644 index 0000000..7160e69 --- /dev/null +++ b/api/src/main/resources/docs/process/创建订单.svg @@ -0,0 +1 @@ +开始下单用户数据是否正常正常查询商品信息商品信息是否正常是否存在是否上架是否下架是否有库存正常创建订单是否成功成功减库存减库存是否成功成功下订单成功结束创建订单 \ No newline at end of file diff --git a/api/src/main/resources/mapper/GoodsMapper.xml b/api/src/main/resources/mapper/GoodsMapper.xml new file mode 100644 index 0000000..d513a46 --- /dev/null +++ b/api/src/main/resources/mapper/GoodsMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + id,category,type, + title,origin_price,price, + stock,limit_count,cover, + description,notice,online_time, + offline_time,create_time,update_time, + status + + + update points_sys.goods + set stock=stock - #{count} + where id = #{id} + and stock >= #{count} + + + + + diff --git a/api/src/main/resources/mapper/OrderInfoMapper.xml b/api/src/main/resources/mapper/OrderInfoMapper.xml new file mode 100644 index 0000000..3120edb --- /dev/null +++ b/api/src/main/resources/mapper/OrderInfoMapper.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/api/src/test/java/me/xiaoyan/point/api/ApiApplicationTests.java b/api/src/test/java/me/xiaoyan/point/api/ApiApplicationTests.java index 2202460..0cbf998 100644 --- a/api/src/test/java/me/xiaoyan/point/api/ApiApplicationTests.java +++ b/api/src/test/java/me/xiaoyan/point/api/ApiApplicationTests.java @@ -2,12 +2,42 @@ package me.xiaoyan.point.api; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.StringRedisTemplate; + +import javax.annotation.Resource; @SpringBootTest class ApiApplicationTests { + @Resource + private StringRedisTemplate stringRedisTemplate; @Test void contextLoads() { } + void testBuy(long buyCount){ + long count = stringRedisTemplate.opsForValue().decrement("a",buyCount); + System.out.println(count); + if(count<0){ + System.out.println("库存不足"); + stringRedisTemplate.opsForValue().increment("a",buyCount); + return; + } + if(count == 0){ + System.out.println("下一次就没有存库了"); + return; + } + System.out.println("购买成功"); + } + + @Test + void testRedis(){ + stringRedisTemplate.opsForValue().set("a","1"); + System.out.println("----------第1次----------"); + testBuy(2); + System.out.println("----------第2次----------"); + testBuy(1); + System.out.println("----------第3次----------"); + testBuy(1); + } }