Merge pull request '秒杀商品' (#5) from czk into main

Reviewed-on: #5
main
czk 2024-04-27 09:45:09 +08:00
commit e51c3ed307
21 changed files with 604 additions and 32 deletions

View File

@ -5,7 +5,6 @@ import cn.hutool.crypto.SecureUtil;
import com.mall.auth.feign.UserServiceFeign;
import com.mall.auth.service.AuthService;
import com.mall.common.constant.JwtConstants;
import com.mall.common.constant.RabbitConstants;
import com.mall.common.constant.TokenConstants;
import com.mall.common.domain.UserInfo;
import com.mall.common.domain.request.LoginRequest;

View File

@ -66,11 +66,11 @@
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis分布式锁 -->
<!-- <dependency>-->
<!-- <groupId>org.redisson</groupId>-->
<!-- <artifactId>redisson</artifactId>-->
<!-- <version>3.16.0</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
<!-- Redis 事务-->
<!-- <dependency>-->
<!-- <groupId>redis.clients</groupId>-->

View File

@ -1,4 +1,7 @@
package com.mall.common.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
@ -15,6 +18,14 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://124.221.183.9:6379");
return Redisson.create(config);
}
@Bean
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory connectionFactory) {

View File

@ -0,0 +1,112 @@
package com.mall.common.domain;
import com.baomidou.mybatisplus.annotation.IdType;
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 java.math.BigDecimal;
/**
*
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("oms_order_item")
public class OrderItemEntity {
/**
* id
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* id
*/
private Long userId;
/**
* order_id
*/
private Long orderId;
/**
* order_sn
*/
private String orderSn;
/**
* spu_id
*/
private Long spuId;
/**
* spu_name
*/
private String spuName;
/**
* spu_pic
*/
private String spuPic;
/**
*
*/
private String spuBrand;
/**
* id
*/
private Long categoryId;
/**
* sku
*/
private Long skuId;
/**
* sku
*/
private String skuName;
/**
* sku
*/
private String skuPic;
/**
* sku
*/
private BigDecimal skuPrice;
/**
*
*/
private Integer skuQuantity;
/**
* JSON
*/
private String skuAttrsVals;
/**
*
*/
private BigDecimal promotionAmount;
/**
*
*/
private BigDecimal couponAmount;
/**
*
*/
private BigDecimal integrationAmount;
/**
*
*/
private BigDecimal realAmount;
/**
*
*/
private Integer giftIntegration;
/**
*
*/
private Integer giftGrowth;
/**
*0->1->2->3->4->5->
*/
private Integer status;
}

View File

@ -89,4 +89,5 @@ public class ActivitySkuVo {
*
*/
private Date endTime;
}

View File

@ -1,11 +1,15 @@
package com.mall.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
*server
*/
@SpringBootApplication
@EnableScheduling
@EnableFeignClients
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class,args);

View File

@ -0,0 +1,98 @@
package com.mall.server.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
/**
* @AuthorChen
* @Packagecom.bw.common.config
* @Projectholiday2
* @nameRabbitConfig
* @Date2024/2/5 10:55
* @Description: TODO
*/
@Log4j2
@Configuration
public class DelayConfig {
/**
*
*/
public static final String CZKQUEUE = "czk_queue";
/**
*
*/
public static final String EXCHANGE = "direct";
/**
* key
*/
public static final String ROUKEYCZK = "roukey_czk_queue";
@Bean
public DirectExchange directExchange() {
return new DirectExchange(EXCHANGE);
}
@Bean
public Queue autoDeleteQueue1(){
HashMap<String, Object> map = new HashMap<>();
map.put("x-dead-letter-exchange",DEADEXCHANGE);
map.put("x-dead-letter-routing-key",DEADROUKEY);
map.put("x-message-ttl",MESSAGE_FIVE);
return new Queue(CZKQUEUE,true,false,false,map);
}
@Bean
public Binding binding1a() {
return BindingBuilder.bind(autoDeleteQueue1())
.to(directExchange())
.with(ROUKEYCZK);
}
/**
*
*/
public static final String DEADEXCHANGE = "dead_exchange";
/**
*
*/
public static final String DEADQUEUE = "dead_queue";
/**
* key
*/
public static final String DEADROUKEY = "dead_dead_queue";
/**
* 5
*/
public static final Integer MESSAGE_FIVE = 5000;
/**
* 10
*/
public static final Integer MESSAGE_FEN = 600000;
@Bean
public DirectExchange deadExchange() {
return new DirectExchange(DEADEXCHANGE);
}
@Bean
public Queue autoDeleteQueue2(){
return new Queue(DEADQUEUE,true);
}
@Bean
public Binding binding2() {
return BindingBuilder.bind(autoDeleteQueue2())
.to(deadExchange())
.with(DEADROUKEY);
}
}

View File

@ -0,0 +1,58 @@
package com.mall.server.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* @CLassName MqConfig
* @Description
* @Author Meng.Wang
* @Date 2023/11/24 21:15
*/
@Configuration
@Log4j2
public class MqConfig implements RabbitTemplate.ReturnsCallback,RabbitTemplate.ConfirmCallback{
public static final String DXQUEUE = "DxQueue";
public static final String DXEXCHANGE = "DxExchange";
public static final String ROUTINGKEY = "RoutingKey";
//创建队列
@Bean
public Queue queue(){
return new Queue(DXQUEUE,true);
}
@Bean("DxExchange")
public DirectExchange directExchange(){
return new DirectExchange(DXEXCHANGE);
}
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(directExchange()).with(ROUTINGKEY);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if(b){
log.info("{}消息到达交换机",correlationData.getId());
}else {
log.error("{}消息丢失",correlationData.getId());
}
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("{}消息未到达队列",returnedMessage.getMessage().getMessageProperties().getMessageId());
}
}

View File

@ -0,0 +1,11 @@
package com.mall.server.constant;
/**
*
*/
public class SpikesConstant {
public static final String SPIKES_INVENTORY = "秒杀商品库存数量";
public static final String SPIKES_SKUID = "秒杀商品Id";
}

View File

@ -2,11 +2,10 @@ package com.mall.server.controller;
import com.mall.common.domain.request.SpikesRequest;
import com.mall.common.result.Result;
import com.mall.server.service.SpikesService;
import org.redisson.client.RedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
/**
*controller
*/
@ -17,6 +16,7 @@ public class SpikesController {
@Autowired
private SpikesService spikesService;
/**
*
* @param spikesRequest
@ -27,4 +27,14 @@ public class SpikesController {
return spikesService.add(spikesRequest);
}
/**
*
* @param skuId
* @return
*/
@GetMapping("spike")
public Result spike(@RequestParam Long skuId){
return spikesService.spike(skuId);
}
}

View File

@ -1,11 +1,7 @@
package com.mall.server.enumerate;
import lombok.Data;
/**
*
*
*/
public enum ActivityEnum {
SPIKES(0,"秒杀未开启"),
BARGAIN(0,"砍价未开启"),

View File

@ -0,0 +1,65 @@
package com.mall.server.job;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mall.common.utils.StringUtils;
import com.mall.server.constant.SpikesConstant;
import com.mall.server.domain.SkuEntity;
import com.mall.server.domain.SpikesEntity;
import com.mall.server.service.SkuService;
import com.mall.server.service.SpikesService;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
*
*/
@Component
public class SpikesJob {
@Autowired
private SkuService skuService;
@Autowired
private SpikesService spikesService;
@Autowired
private RedissonClient redissonClient;
// @Scheduled(cron = "0/5 * * * * ? ")
public void spikesJob(){
Date date = Date.from(
LocalDateTime.now().withHour(0).withMinute(0)
.withSecond(0).withNano(0)
.plusDays(1).atZone(ZoneId.systemDefault()).toInstant()
);
List<SpikesEntity> list = spikesService.list(
new LambdaQueryWrapper<SpikesEntity>()
.ge(SpikesEntity::getCreateTime, date)
);
if(CollectionUtils.isEmpty(list)){
return;
}
list.forEach(c->{
skuService.list(
new LambdaQueryWrapper<SkuEntity>()
.eq(c.getSpikesId()!=null,SkuEntity::getSpikesId,c.getSpikesId())
).forEach(s -> {
RSemaphore semaphore = redissonClient.getSemaphore(SpikesConstant.SPIKES_INVENTORY + s.getId());
semaphore.trySetPermitsAsync(s.getInventoryRestrict());
long expire = (c.getEndTime().getTime() - System.currentTimeMillis()) / 60000;
semaphore.expire(expire, TimeUnit.MINUTES);
});
});
}
}

View File

@ -0,0 +1,12 @@
package com.mall.server.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mall.common.domain.OrderItemEntity;
import org.apache.ibatis.annotations.Mapper;
/**
*Mapper
*/
@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItemEntity> {
}

View File

@ -0,0 +1,55 @@
package com.mall.server.monitor;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mall.common.domain.OrderItemEntity;
import com.mall.server.config.DelayConfig;
import com.mall.server.constant.SpikesConstant;
import com.mall.server.service.OrderItemService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
*
*/
@Component
@Slf4j
public class SpikesMonitor {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderItemService orderItemService;
@RabbitListener(queues = DelayConfig.DEADQUEUE)
public void consumer(String meg, Message message, Channel channel){
log.info("延迟队列接受到消息"+meg);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String[] split = meg.split(",");
String skuId = split[0];
String orderSn = split[1];
OrderItemEntity orderItemEntity = orderItemService.getOne(
new LambdaQueryWrapper<OrderItemEntity>()
.eq(OrderItemEntity::getOrderSn, orderSn)
);
if (orderItemEntity.getStatus().equals(0)) {
RSemaphore semaphore = redissonClient.getSemaphore(SpikesConstant.SPIKES_INVENTORY + skuId);
semaphore.release(1);
}
try {
channel.basicAck(deliveryTag,false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,10 @@
package com.mall.server.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mall.common.domain.OrderItemEntity;
/**
*Service
*/
public interface OrderItemService extends IService<OrderItemEntity> {
}

View File

@ -11,4 +11,6 @@ import com.mall.server.domain.SpikesEntity;
public interface SpikesService extends IService<SpikesEntity> {
Result add(SpikesRequest spikesRequest);
Result spike(Long skuId);
}

View File

@ -0,0 +1,19 @@
package com.mall.server.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mall.common.domain.OrderItemEntity;
import com.mall.server.mapper.OrderItemMapper;
import com.mall.server.service.OrderItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*ServiceImpl
*/
@Service
public class OrderItemServiceImpl extends ServiceImpl<OrderItemMapper, OrderItemEntity>
implements OrderItemService {
@Autowired
private OrderItemMapper orderItemMapper;
}

View File

@ -1,25 +1,39 @@
package com.mall.server.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mall.common.constant.TokenConstants;
import com.mall.common.domain.OrderItemEntity;
import com.mall.common.domain.UserInfo;
import com.mall.common.domain.request.SpikesRequest;
import com.mall.common.domain.vo.ActivitySkuVo;
import com.mall.common.redis.RedisCache;
import com.mall.common.result.BizException;
import com.mall.common.result.Result;
import com.mall.common.utils.IdUtils;
import com.mall.common.utils.StringUtils;
import com.mall.server.config.DelayConfig;
import com.mall.server.constant.SpikesConstant;
import com.mall.server.domain.SkuEntity;
import com.mall.server.domain.SpikesEntity;
import com.mall.server.enumerate.ActivityEnum;
import com.mall.server.mapper.SpikesMapper;
import com.mall.server.service.OrderItemService;
import com.mall.server.service.SkuService;
import com.mall.server.service.SpikesService;
import com.mall.server.service.SpuService;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -34,9 +48,6 @@ public class SpikesServiceImpl extends ServiceImpl<SpikesMapper, SpikesEntity>
@Autowired
private SpikesMapper spikesMapper;
@Autowired
private SpuService spuService;
@Autowired
private SkuService skuService;
@ -44,11 +55,38 @@ public class SpikesServiceImpl extends ServiceImpl<SpikesMapper, SpikesEntity>
private RedisCache redisCache;
@Autowired
private RedisTemplate redisTemplate;
private RedissonClient redissonClient;
@Autowired
private HttpServletRequest request;
@Autowired
private OrderItemService orderItemService;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
*
* @param spikesRequest
* @return
*/
@Transactional
@Override
public Result add(SpikesRequest spikesRequest) {
if(spikesRequest.getCreateTime().getMinutes()%60!=0){
throw new BizException(500,"不是整点");
}
//一天后的0点
Date date = Date.from(
LocalDateTime.now().withHour(0).withMinute(0)
.withSecond(0).withNano(0)
.plusDays(1).atZone(ZoneId.systemDefault()).toInstant()
);
if(spikesRequest.getCreateTime().compareTo(date)<0){
throw new BizException(500,"时间必须是第二天0点");
}
//秒杀表信息
int spikesId = spikesMapper.insert(
SpikesEntity.builder()
@ -96,12 +134,84 @@ public class SpikesServiceImpl extends ServiceImpl<SpikesMapper, SpikesEntity>
.bargainId(c.getBargainId())
.groupId(c.getGroupId())
.startTime(spikesRequest.getCreateTime())
.endTime(spikesRequest.getEndTime()).build()).collect(Collectors.toList());
.endTime(spikesRequest.getEndTime()).build()
).collect(Collectors.toList());
//后期存入redis放入查询列表中
activitySkuVoList.forEach(c->{
//redis秒杀信息存在时间
long expire = c.getEndTime().getTime() - System.currentTimeMillis()/1000/1000;
redisCache.setCacheObject("spikes_"+c.getId(),c,expire, TimeUnit.MINUTES);
long expire = (c.getEndTime().getTime() - System.currentTimeMillis()) / 60000;
redisCache.setCacheObject(SpikesConstant.SPIKES_SKUID +c.getId(),c,expire, TimeUnit.MINUTES);
});
return Result.success(true,"添加秒杀成功");
}
/**
*
* @param skuId
* @return
*/
@Override
public Result spike(Long skuId) {
SkuEntity skuEntity = skuService.getOne(
new LambdaQueryWrapper<SkuEntity>()
.eq(skuId != null, SkuEntity::getId, skuId)
);
SpikesEntity spikesEntity = spikesMapper.selectOne(
new LambdaQueryWrapper<SpikesEntity>()
.eq(skuEntity.getSpikesId() != null, SpikesEntity::getSpikesId, skuEntity.getSpikesId())
);
if(spikesEntity.getCreateTime().compareTo(new Date())>0 || spikesEntity.getEndTime().compareTo(new Date())<0){
throw new BizException(500,skuEntity.getName()+"该商品不在抢购时间内");
}
UserInfo login = getLogin();
OrderItemEntity orderItemEntity = orderItemService.getOne(
new LambdaQueryWrapper<OrderItemEntity>()
.eq(OrderItemEntity::getUserId, login.getId())
.eq(OrderItemEntity::getSkuId,skuId)
);
if(orderItemEntity!=null){
throw new BizException(500,"禁止重复抢购欧");
}
RSemaphore semaphore = redissonClient.getSemaphore(SpikesConstant.SPIKES_INVENTORY + skuId);
// if(semaphore.availablePermits()==0){
// //同步数据库库存量
// throw new BizException(500,"商品已抢购完");
// }
if(!semaphore.tryAcquire(1)){
throw new BizException(500,"系统繁忙,商品已抢购完");
}
String orderSn = IdUtils.genId();
orderItemService.save(
OrderItemEntity.builder()
.userId(login.getId())
.orderSn(orderSn)
.skuId(skuId)
.skuName(skuEntity.getName())
.skuPic(skuEntity.getDefaultImage())
.skuPrice(skuEntity.getPrice())
// .promotionAmount()
// .couponAmount()
// .integrationAmount()
.realAmount(skuEntity.getActivityPrice())
.status(0).build()
);
rabbitTemplate.convertAndSend(
DelayConfig.EXCHANGE, DelayConfig.ROUKEYCZK, skuId + "," +orderSn ,
message -> {
message.getMessageProperties().setMessageId(IdUtils.genId());
return message;
}
);
return Result.success(skuId,"下单成功");
}
public UserInfo getLogin(){
String token = request.getHeader(TokenConstants.LOGIN_TOKEN_KEY);
if (StringUtils.isBlank(token)){
throw new BizException(401,"未登录");
}
UserInfo userInfo = redisCache.getCacheObject(TokenConstants.LOGIN_TOKEN_KEY + token);
return userInfo;
}
}

View File

@ -12,7 +12,7 @@ spring:
listener:
simple:
# 消息确认模式这里设置为手动确认manual即消费者需要手动确认消息的消费
acknowledge-mode: auto
acknowledge-mode: manual
main:
allow-circular-references: true
jackson:

View File

@ -27,8 +27,7 @@ public class UserController {
@PostMapping("findLogin/{username}/{password}")
public Result<UserInfo> findLogin(@PathVariable String username, @PathVariable String password){
LambdaQueryWrapper<UserInfo> userInfoLambdaQueryWrapper = new LambdaQueryWrapper<>();
userInfoLambdaQueryWrapper.eq(UserInfo::getUsername,username)
.eq(UserInfo::getPassword,password);
userInfoLambdaQueryWrapper.eq(UserInfo::getUsername,username);
UserInfo userInfo = userService.getOne(userInfoLambdaQueryWrapper);
return Result.success(userInfo);
}

View File

@ -2,7 +2,7 @@ package com.mall.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mall.common.domain.UserInfo;
import org.mapstruct.Mapper;
import org.apache.ibatis.annotations.Mapper;
/**
* @Author: lzh