xiongzhu 4 سال پیش
والد
کامیت
c700ca44b1

+ 5 - 0
src/main/java/com/izouma/nineth/domain/Order.java

@@ -8,6 +8,7 @@ import com.izouma.nineth.annotations.Searchable;
 import com.izouma.nineth.converter.FileObjectListConverter;
 import com.izouma.nineth.converter.PrivilegeListConverter;
 import com.izouma.nineth.converter.PropertyListConverter;
+import com.izouma.nineth.enums.CollectionSource;
 import com.izouma.nineth.enums.CollectionType;
 import com.izouma.nineth.enums.OrderStatus;
 import com.izouma.nineth.enums.PayMethod;
@@ -117,6 +118,10 @@ public class Order {
     @Enumerated(EnumType.STRING)
     private CollectionType type;
 
+    @ApiModelProperty("来源")
+    @Enumerated(EnumType.STRING)
+    private CollectionSource source;
+
     @ApiModelProperty("铸造者")
     @Searchable
     private String minter;

+ 4 - 0
src/main/java/com/izouma/nineth/event/CreateOrderEvent.java

@@ -1,8 +1,12 @@
 package com.izouma.nineth.event;
 
+import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 @Data
+@AllArgsConstructor
+@NoArgsConstructor
 public class CreateOrderEvent {
     private Long userId;
     private Long collectionId;

+ 6 - 3
src/main/java/com/izouma/nineth/service/CreateOrderListener.java → src/main/java/com/izouma/nineth/listener/CreateOrderListener.java

@@ -1,16 +1,19 @@
-package com.izouma.nineth.service;
+package com.izouma.nineth.listener;
 
 import com.izouma.nineth.event.CreateOrderEvent;
+import com.izouma.nineth.service.OrderService;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.ConsumeMode;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
 import org.apache.rocketmq.spring.core.RocketMQListener;
 import org.springframework.stereotype.Service;
 
 @Service
 @Slf4j
 @AllArgsConstructor
-//@RocketMQMessageListener(
-//        consumerGroup = "${rocketmq.consumer.group}", topic = "test-add-topic", consumeMode = ConsumeMode.ORDERLY)
+@RocketMQMessageListener(
+        consumerGroup = "${general.create-order-group}", topic = "${general.create-order-topic}", consumeMode = ConsumeMode.CONCURRENTLY)
 public class CreateOrderListener implements RocketMQListener<CreateOrderEvent> {
     private OrderService orderService;
 

+ 2 - 3
src/main/java/com/izouma/nineth/service/TestConsumer.java → src/main/java/com/izouma/nineth/listener/TestConsumer.java

@@ -1,4 +1,4 @@
-package com.izouma.nineth.service;
+package com.izouma.nineth.listener;
 
 import com.izouma.nineth.event.MyMqEvent;
 import lombok.AllArgsConstructor;
@@ -13,12 +13,11 @@ import org.springframework.stereotype.Service;
 @Slf4j
 @AllArgsConstructor
 @RocketMQMessageListener(
-        consumerGroup = "${rocketmq.consumer.group}", topic = "test-add-topic", consumeMode = ConsumeMode.ORDERLY)
+        consumerGroup = "test-group", topic = "test-add-topic", consumeMode = ConsumeMode.CONCURRENTLY)
 public class TestConsumer implements RocketMQListener<MyMqEvent> {
 
     @SneakyThrows
     public void onMessage(MyMqEvent event) {
-        Thread.sleep((long) (Math.random() * 200));
         log.info("receive message: {}", event.getData());
     }
 }

+ 0 - 26
src/main/java/com/izouma/nineth/lock/RedisLockException.java

@@ -1,26 +0,0 @@
-package com.izouma.nineth.lock;
-
-/**
- * Created by h p on 2017/3/9.
- */
-public class RedisLockException extends RuntimeException {
-
-    public RedisLockException() {
-    }
-
-    public RedisLockException(String message) {
-        super(message);
-    }
-
-    public RedisLockException(String message, Throwable cause) {
-        super(message, cause);
-    }
-
-    public RedisLockException(Throwable cause) {
-        super(cause);
-    }
-
-    public RedisLockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
-        super(message, cause, enableSuppression, writableStackTrace);
-    }
-}

+ 0 - 110
src/main/java/com/izouma/nineth/lock/RedisLockInterceptor.java

@@ -1,110 +0,0 @@
-package com.izouma.nineth.lock;
-
-import com.google.common.base.Joiner;
-import com.izouma.nineth.utils.RedisLockUtil;
-import org.apache.commons.lang3.StringUtils;
-import org.aspectj.lang.ProceedingJoinPoint;
-import org.aspectj.lang.annotation.Around;
-import org.aspectj.lang.annotation.Aspect;
-import org.aspectj.lang.annotation.Pointcut;
-import org.aspectj.lang.reflect.MethodSignature;
-import org.springframework.aop.support.AopUtils;
-import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
-import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.expression.spel.support.StandardEvaluationContext;
-import org.springframework.stereotype.Component;
-
-import java.lang.reflect.Method;
-import java.util.concurrent.TimeUnit;
-
-/**
- * @author mashaohua
- */
-@Aspect
-@Component
-public class RedisLockInterceptor {
-
-    private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
-
-    private static final ExpressionParser PARSER = new SpelExpressionParser();
-
-    @Pointcut("@annotation(com.izouma.nineth.lock.RedisLockable)")
-    public void pointcut() {
-    }
-
-    @Around("pointcut()")
-    public Object doAround(ProceedingJoinPoint point) throws Throwable {
-
-        MethodSignature methodSignature = (MethodSignature) point.getSignature();
-        Method targetMethod = AopUtils.getMostSpecificMethod(methodSignature.getMethod(), point.getTarget().getClass());
-        String targetName = point.getTarget().getClass().getName();
-        String methodName = point.getSignature().getName();
-        Object[] arguments = point.getArgs();
-
-        RedisLockable redisLock = targetMethod.getAnnotation(RedisLockable.class);
-        long expire = redisLock.expiration();
-        String redisKey = getLockKey(redisLock, targetMethod, targetName, methodName, arguments);
-        String uuid;
-        if (redisLock.isWaiting()) {
-            uuid = waitingLock(redisKey, expire, redisLock.retryCount(), redisLock.retryWaitingTime());
-        } else {
-            uuid = noWaitingLock(redisKey, expire);
-        }
-        if (StringUtils.isNotEmpty(uuid)) {
-            try {
-                return point.proceed();
-            } finally {
-                RedisLockUtil.unLock(redisKey, uuid);
-            }
-        } else {
-            throw new RedisLockException(redisKey);
-        }
-    }
-
-    private String getLockKey(RedisLockable redisLock, Method targetMethod,
-                              String targetName, String methodName, Object[] arguments) {
-        String[] keys = redisLock.key();
-        String prefix = redisLock.prefix();
-        StringBuilder sb = new StringBuilder("lock.");
-        if (StringUtils.isEmpty(prefix)) {
-            sb.append(targetName).append(".").append(methodName);
-        } else {
-            sb.append(prefix);
-        }
-        if (keys != null) {
-            String keyStr = Joiner.on("+ '.' +").skipNulls().join(keys);
-            EvaluationContext context = new StandardEvaluationContext(targetMethod);
-            String[] parameterNames = DISCOVERER.getParameterNames(targetMethod);
-            for (int i = 0; i < parameterNames.length; i++) {
-                context.setVariable(parameterNames[i], arguments[i]);
-            }
-            Object key = PARSER.parseExpression(keyStr).getValue(context);
-            sb.append("#").append(key);
-        }
-        return sb.toString();
-    }
-
-    private String noWaitingLock(String key, long expire) {
-        return RedisLockUtil.lock(key, expire);
-    }
-
-    private String waitingLock(String key, long expire, int retryCount, int retryWaitingTime)
-            throws InterruptedException {
-        int count = 0;
-        while (retryCount == -1 || count <= retryCount) {
-            String uuid = noWaitingLock(key, expire);
-            if (!StringUtils.isEmpty(uuid)) {
-                return uuid;
-            }
-            try {
-                TimeUnit.MILLISECONDS.sleep(retryWaitingTime);
-            } catch (InterruptedException e) {
-                throw e;
-            }
-            count++;
-        }
-        return null;
-    }
-}

+ 0 - 26
src/main/java/com/izouma/nineth/lock/RedisLockable.java

@@ -1,26 +0,0 @@
-package com.izouma.nineth.lock;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * @author mashaohua
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface RedisLockable {
-
-    String prefix() default "";
-
-    String[] key() default "";
-
-    long expiration() default 60;
-
-    boolean isWaiting() default false; //锁是否等待,默认为不等待
-
-    int retryCount() default -1; // 锁等待重试次数,-1未不限制
-
-    int retryWaitingTime() default 10; // 锁等待重试间隔时间,默认10毫秒
-}

+ 2 - 12
src/main/java/com/izouma/nineth/repo/CollectionRepo.java

@@ -65,18 +65,6 @@ public interface CollectionRepo extends JpaRepository<Collection, Long>, JpaSpec
     @CacheEvict(value = "collection", key = "#id")
     void increaseNumber(Long id, int d);
 
-    @Transactional
-    @Modifying
-    @Query("update Collection c set c.sale = COALESCE(c.sale, 0) + ?2 where c.id = ?1")
-    @CacheEvict(value = "collection", key = "#id")
-    void increaseSale(Long id, int d);
-
-    @Transactional
-    @Modifying
-    @Query("update Collection c set c.stock = COALESCE(c.stock, 0) + ?2 where c.id = ?1")
-    @CacheEvict(value = "collection", key = "#id")
-    void increaseStock(Long id, int d);
-
     @Transactional
     @Modifying
     @Query("update Collection c set c.total = COALESCE(c.total, 0) + ?2 where c.id = ?1")
@@ -101,4 +89,6 @@ public interface CollectionRepo extends JpaRepository<Collection, Long>, JpaSpec
     List<Collection> findByScheduleSaleTrue();
 
     List<Collection> findByNameLike(String name);
+
+    List<Collection> findByStockGreaterThan(int stock);
 }

+ 1 - 0
src/main/java/com/izouma/nineth/security/WebSecurityConfig.java

@@ -99,6 +99,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .antMatchers("/appVersion/checkIosReview").permitAll()
                 .antMatchers("/druid/**").permitAll()
                 .antMatchers("/testmq/**").permitAll()
+                .antMatchers("/teststock/**").permitAll()
                 // all other requests need to be authenticated
                 .anyRequest().authenticated().and()
                 // make sure we use stateless session; session won't be used to

+ 2 - 2
src/main/java/com/izouma/nineth/service/AirDropService.java

@@ -60,8 +60,8 @@ public class AirDropService {
                     } else {
                         assetService.createAsset(collection, user, null, null, "空投", collectionService.getNextNumber(collection.getId()));
                     }
-                    collectionRepo.increaseStock(collection.getId(), -1);
-                    collectionRepo.increaseSale(collection.getId(), 1);
+                    collectionService.decreaseStock(collection.getId(), 1);
+                    collectionService.increaseSale(collection.getId(), 1);
                 } catch (Exception e) {
                     log.error("空投出错", e);
                 }

+ 0 - 8
src/main/java/com/izouma/nineth/service/AssetService.java

@@ -10,7 +10,6 @@ import com.izouma.nineth.enums.CollectionType;
 import com.izouma.nineth.enums.OrderStatus;
 import com.izouma.nineth.event.TransferAssetEvent;
 import com.izouma.nineth.exception.BusinessException;
-import com.izouma.nineth.lock.RedisLockable;
 import com.izouma.nineth.repo.*;
 import com.izouma.nineth.utils.JpaUtils;
 import com.izouma.nineth.utils.SecurityUtils;
@@ -24,7 +23,6 @@ import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import org.springframework.web.bind.annotation.RequestParam;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
@@ -316,12 +314,6 @@ public class AssetService {
         return tokenHistoryRepo.findByTokenIdOrderByCreatedAtDesc(tokenId);
     }
 
-    @RedisLockable(key = "#id", expiration = 60, isWaiting = true)
-    public void testLock(String id, String i) throws InterruptedException {
-        Thread.sleep(1000);
-        log.info("" + i);
-    }
-
     public void setHistory() {
         List<Asset> assets = assetRepo.findByCreatedAtBefore(LocalDateTime.of(2021, 11, 22, 23, 59, 59));
         assets.parallelStream().forEach(asset -> {

+ 38 - 10
src/main/java/com/izouma/nineth/service/CollectionService.java

@@ -18,12 +18,14 @@ import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.RandomUtils;
 import org.apache.commons.lang3.Range;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.beans.BeanUtils;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.TaskScheduler;
 import org.springframework.stereotype.Service;
 
@@ -41,14 +43,16 @@ import java.util.stream.Collectors;
 @Slf4j
 public class CollectionService {
 
-    private CollectionRepo   collectionRepo;
-    private LikeRepo         likeRepo;
-    private BlindBoxItemRepo blindBoxItemRepo;
-    private AppointmentRepo  appointmentRepo;
-    private UserRepo         userRepo;
-    private AssetService     assetService;
-    private TaskScheduler    taskScheduler;
-    private CacheService     cacheService;
+    private CollectionRepo                collectionRepo;
+    private LikeRepo                      likeRepo;
+    private BlindBoxItemRepo              blindBoxItemRepo;
+    private AppointmentRepo               appointmentRepo;
+    private UserRepo                      userRepo;
+    private AssetService                  assetService;
+    private TaskScheduler                 taskScheduler;
+    private CacheService                  cacheService;
+    private RedisTemplate<String, Object> redisTemplate;
+    private RocketMQTemplate              rocketMQTemplate;
 
     private final Map<Long, ScheduledFuture<?>> tasks = new HashMap<>();
 
@@ -58,6 +62,10 @@ public class CollectionService {
         for (Collection collection : collections) {
             onShelfTask(collection);
         }
+        for (Collection collection : collectionRepo.findByStockGreaterThan(0)) {
+            redisTemplate.opsForValue().set("collectionStock::" + collection.getId(), collection.getStock());
+            redisTemplate.opsForValue().set("collectionSale::" + collection.getId(), collection.getStock());
+        }
     }
 
     public Page<Collection> all(PageQuery pageQuery) {
@@ -226,7 +234,7 @@ public class CollectionService {
         createBlindBox.getItems().stream().parallel().forEach(item -> {
             Collection collection = list.stream().filter(i -> i.getId().equals(item.getCollectionId())).findAny()
                     .orElseThrow(new BusinessException("所选藏品不存在"));
-            collectionRepo.increaseStock(collection.getId(), -item.getTotal());
+            decreaseStock(collection.getId(), item.getTotal());
             BlindBoxItem blindBoxItem = new BlindBoxItem();
             BeanUtils.copyProperties(collection, blindBoxItem);
             blindBoxItem.setId(null);
@@ -325,7 +333,27 @@ public class CollectionService {
         if (collection.getType() == CollectionType.BLIND_BOX) {
             throw new BusinessException("盲盒无法增发");
         }
-        collectionRepo.increaseStock(id, number);
+        increaseStock(id, number);
         collectionRepo.increaseTotal(id, number);
     }
+
+    public Long increaseStock(Long id, int number) {
+        Long stock = redisTemplate.opsForValue().increment("collectionStock::" + id, number);
+        return stock;
+    }
+
+    public Long decreaseStock(Long id, int number) {
+        Long stock = redisTemplate.opsForValue().decrement("collectionStock::" + id, number);
+        return stock;
+    }
+
+    public Long increaseSale(Long id, int number) {
+        Long sale = redisTemplate.opsForValue().increment("collectionSale::" + id, number);
+        return sale;
+    }
+
+    public Long decreaseSale(Long id, int number) {
+        Long sale = redisTemplate.opsForValue().decrement("collectionSale::" + id, number);
+        return sale;
+    }
 }

+ 132 - 119
src/main/java/com/izouma/nineth/service/OrderService.java

@@ -24,6 +24,7 @@ import com.izouma.nineth.domain.*;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.enums.*;
 import com.izouma.nineth.event.CreateAssetEvent;
+import com.izouma.nineth.event.CreateOrderEvent;
 import com.izouma.nineth.event.TransferAssetEvent;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.*;
@@ -36,10 +37,11 @@ import org.apache.commons.codec.EncoderException;
 import org.apache.commons.codec.net.URLCodec;
 import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.context.event.EventListener;
 import org.springframework.core.env.Environment;
 import org.springframework.data.domain.Page;
-import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.ui.Model;
@@ -56,135 +58,146 @@ import java.util.*;
 @Slf4j
 public class OrderService {
 
-    private OrderRepo                     orderRepo;
-    private CollectionRepo                collectionRepo;
-    private UserAddressRepo               userAddressRepo;
-    private UserRepo                      userRepo;
-    private Environment                   env;
-    private AlipayClient                  alipayClient;
-    private AlipayProperties              alipayProperties;
-    private WxPayService                  wxPayService;
-    private WxPayProperties               wxPayProperties;
-    private AssetService                  assetService;
-    private SysConfigService              sysConfigService;
-    private BlindBoxItemRepo              blindBoxItemRepo;
-    private AssetRepo                     assetRepo;
-    private UserCouponRepo                userCouponRepo;
-    private CollectionService             collectionService;
-    private RedisTemplate<String, Object> redisTemplate;
-    private CommissionRecordRepo          commissionRecordRepo;
-    private AdapayProperties              adapayProperties;
-    private GeneralProperties             generalProperties;
+    private OrderRepo            orderRepo;
+    private CollectionRepo       collectionRepo;
+    private UserAddressRepo      userAddressRepo;
+    private UserRepo             userRepo;
+    private Environment          env;
+    private AlipayClient         alipayClient;
+    private AlipayProperties     alipayProperties;
+    private WxPayService         wxPayService;
+    private WxPayProperties      wxPayProperties;
+    private AssetService         assetService;
+    private SysConfigService     sysConfigService;
+    private BlindBoxItemRepo     blindBoxItemRepo;
+    private AssetRepo            assetRepo;
+    private UserCouponRepo       userCouponRepo;
+    private CollectionService    collectionService;
+    private CommissionRecordRepo commissionRecordRepo;
+    private AdapayProperties     adapayProperties;
+    private GeneralProperties    generalProperties;
+    private RocketMQTemplate     rocketMQTemplate;
 
     public Page<Order> all(PageQuery pageQuery) {
         return orderRepo.findAll(JpaUtils.toSpecification(pageQuery, Order.class), JpaUtils.toPageRequest(pageQuery));
     }
 
+    public Object mqCreate(Long userId, Long collectionId, int qty, Long addressId, Long userCouponId, Long invitor) {
+        SendResult result = rocketMQTemplate.syncSend("create-order", new CreateOrderEvent(userId, collectionId, qty, addressId, userCouponId, invitor));
+        return result.getMsgId();
+    }
+
     @Transactional
     public Order create(Long userId, Long collectionId, int qty, Long addressId, Long userCouponId, Long invitor) {
-        if (qty <= 0) throw new BusinessException("数量必须大于0");
-        Collection collection = collectionRepo.findById(collectionId).orElseThrow(new BusinessException("藏品不存在"));
-        User minter = userRepo.findById(collection.getMinterId()).orElseThrow(new BusinessException("铸造者不存在"));
-        UserCoupon coupon = null;
-        if (userCouponId != null) {
-            coupon = userCouponRepo.findById(userCouponId).orElseThrow(new BusinessException("兑换券不存在"));
-            if (coupon.isUsed()) {
-                throw new BusinessException("该兑换券已使用");
+        qty = 1;
+        int stock = Optional.ofNullable(collectionService.decreaseStock(collectionId, qty))
+                .map(Math::toIntExact)
+                .orElseThrow(new BusinessException("很遗憾,藏品已售罄"));
+        try {
+            if (stock < 0) {
+                throw new BusinessException("很遗憾,藏品已售罄");
             }
-            if (coupon.isLimited() && !coupon.getCollectionIds().contains(collectionId)) {
-                throw new BusinessException("该兑换券不可用");
+            Collection collection = collectionRepo.findById(collectionId).orElseThrow(new BusinessException("藏品不存在"));
+            User minter = userRepo.findById(collection.getMinterId()).orElseThrow(new BusinessException("铸造者不存在"));
+            UserCoupon coupon = null;
+            if (userCouponId != null) {
+                coupon = userCouponRepo.findById(userCouponId).orElseThrow(new BusinessException("兑换券不存在"));
+                if (coupon.isUsed()) {
+                    throw new BusinessException("该兑换券已使用");
+                }
+                if (coupon.isLimited() && !coupon.getCollectionIds().contains(collectionId)) {
+                    throw new BusinessException("该兑换券不可用");
+                }
             }
-        }
-        if (collection.isScheduleSale()) {
-            if (collection.getStartTime().isAfter(LocalDateTime.now())) {
-                throw new BusinessException("当前还未开售");
+            if (collection.isScheduleSale()) {
+                if (collection.getStartTime().isAfter(LocalDateTime.now())) {
+                    throw new BusinessException("当前还未开售");
+                }
             }
-        }
-        if (!collection.isSalable()) {
-            throw new BusinessException("该藏品当前不可购买");
-        }
-        if (!collection.isOnShelf()) {
-            if (!collection.isScanCode()) {
-                throw new BusinessException("藏品已下架");
+            if (!collection.isOnShelf()) {
+                if (!collection.isScanCode()) {
+                    throw new BusinessException("藏品已下架");
+                }
             }
-        }
-        if (qty > collection.getStock()) {
-            throw new BusinessException("库存不足");
-        }
-
 
-        if (collection.getMaxCount() > 0) {
-            int count;
-            if (StringUtils.isNotBlank(collection.getCountId())) {
-                count = orderRepo.countByUserIdAndCountIdAndStatusIn(userId, collection.getCountId(), Arrays.asList(OrderStatus.FINISH, OrderStatus.NOT_PAID, OrderStatus.PROCESSING));
-            } else {
-                count = orderRepo.countByUserIdAndCollectionIdAndStatusIn(userId, collectionId, Arrays.asList(OrderStatus.FINISH, OrderStatus.NOT_PAID, OrderStatus.PROCESSING));
+            if (!collection.isSalable()) {
+                throw new BusinessException("该藏品当前不可购买");
             }
-            if (count >= collection.getMaxCount()) {
-                throw new BusinessException("限购" + collection.getMaxCount() + "件");
+
+            if (collection.getMaxCount() > 0) {
+                int count;
+                if (StringUtils.isNotBlank(collection.getCountId())) {
+                    count = orderRepo.countByUserIdAndCountIdAndStatusIn(userId, collection.getCountId(), Arrays.asList(OrderStatus.FINISH, OrderStatus.NOT_PAID, OrderStatus.PROCESSING));
+                } else {
+                    count = orderRepo.countByUserIdAndCollectionIdAndStatusIn(userId, collectionId, Arrays.asList(OrderStatus.FINISH, OrderStatus.NOT_PAID, OrderStatus.PROCESSING));
+                }
+                if (count >= collection.getMaxCount()) {
+                    throw new BusinessException("限购" + collection.getMaxCount() + "件");
+                }
             }
-        }
 
-        UserAddress userAddress = null;
-        if (addressId != null) {
-            userAddress = userAddressRepo.findById(addressId).orElseThrow(new BusinessException("地址信息不存在"));
-        }
+            UserAddress userAddress = null;
+            if (addressId != null) {
+                userAddress = userAddressRepo.findById(addressId).orElseThrow(new BusinessException("地址信息不存在"));
+            }
 
-        collectionRepo.increaseStock(collectionId, -qty);
-        collectionRepo.increaseSale(collectionId, qty);
-
-        BigDecimal gasFee = sysConfigService.getBigDecimal("gas_fee");
-        Order order = Order.builder()
-                .userId(userId)
-                .collectionId(collectionId)
-                .name(collection.getName())
-                .pic(collection.getPic())
-                .detail(collection.getDetail())
-                .properties(collection.getProperties())
-                .category(collection.getCategory())
-                .canResale(collection.isCanResale())
-                .royalties(collection.getRoyalties())
-                .serviceCharge(collection.getServiceCharge())
-                .type(collection.getType())
-                .minterId(collection.getMinterId())
-                .minter(minter.getNickname())
-                .minterAvatar(minter.getAvatar())
-                .qty(qty)
-                .price(collection.getPrice())
-                .gasPrice(gasFee)
-                .totalPrice(collection.getPrice().multiply(BigDecimal.valueOf(qty)).add(gasFee))
-                .contactName(Optional.ofNullable(userAddress).map(UserAddress::getName).orElse(null))
-                .contactPhone(Optional.ofNullable(userAddress).map(UserAddress::getPhone).orElse(null))
-                .address(Optional.ofNullable(userAddress).map(u ->
-                                u.getProvinceId() + " " + u.getCityId() + " " + u.getDistrictId() + " " + u.getAddress())
-                        .orElse(null))
-                .status(OrderStatus.NOT_PAID)
-                .assetId(collection.getAssetId())
-                .couponId(userCouponId)
-                .invitor(invitor)
-                .countId(collection.getCountId())
-                .build();
-        if (coupon != null) {
-            coupon.setUsed(true);
-            coupon.setUseTime(LocalDateTime.now());
-            if (coupon.isNeedGas()) {
-                order.setTotalPrice(order.getGasPrice());
-            } else {
-                order.setTotalPrice(BigDecimal.ZERO);
+            BigDecimal gasFee = sysConfigService.getBigDecimal("gas_fee");
+            Order order = Order.builder()
+                    .userId(userId)
+                    .collectionId(collectionId)
+                    .name(collection.getName())
+                    .pic(collection.getPic())
+                    .detail(collection.getDetail())
+                    .properties(collection.getProperties())
+                    .category(collection.getCategory())
+                    .canResale(collection.isCanResale())
+                    .royalties(collection.getRoyalties())
+                    .serviceCharge(collection.getServiceCharge())
+                    .type(collection.getType())
+                    .source(collection.getSource())
+                    .minterId(collection.getMinterId())
+                    .minter(minter.getNickname())
+                    .minterAvatar(minter.getAvatar())
+                    .qty(qty)
+                    .price(collection.getPrice())
+                    .gasPrice(gasFee)
+                    .totalPrice(collection.getPrice().multiply(BigDecimal.valueOf(qty)).add(gasFee))
+                    .contactName(Optional.ofNullable(userAddress).map(UserAddress::getName).orElse(null))
+                    .contactPhone(Optional.ofNullable(userAddress).map(UserAddress::getPhone).orElse(null))
+                    .address(Optional.ofNullable(userAddress).map(u ->
+                                    u.getProvinceId() + " " + u.getCityId() + " " + u.getDistrictId() + " " + u.getAddress())
+                            .orElse(null))
+                    .status(OrderStatus.NOT_PAID)
+                    .assetId(collection.getAssetId())
+                    .couponId(userCouponId)
+                    .invitor(invitor)
+                    .countId(collection.getCountId())
+                    .build();
+            if (coupon != null) {
+                coupon.setUsed(true);
+                coupon.setUseTime(LocalDateTime.now());
+                if (coupon.isNeedGas()) {
+                    order.setTotalPrice(order.getGasPrice());
+                } else {
+                    order.setTotalPrice(BigDecimal.ZERO);
+                }
             }
-        }
 
-        if (collection.getSource() == CollectionSource.TRANSFER) {
-            Asset asset = assetRepo.findById(collection.getAssetId()).orElseThrow(new BusinessException("资产不存在"));
-            asset.setStatus(AssetStatus.TRADING);
-            assetRepo.save(asset);
-            collectionRepo.setOnShelf(collectionId, false);
-        }
-        order = orderRepo.save(order);
-        if (order.getTotalPrice().equals(BigDecimal.ZERO)) {
-            notifyOrder(order.getId(), PayMethod.WEIXIN, null);
+            if (collection.getSource() == CollectionSource.TRANSFER) {
+                Asset asset = assetRepo.findById(collection.getAssetId()).orElseThrow(new BusinessException("资产不存在"));
+                asset.setStatus(AssetStatus.TRADING);
+                assetRepo.save(asset);
+                collectionRepo.setOnShelf(collectionId, false);
+            }
+            order = orderRepo.save(order);
+            if (order.getTotalPrice().equals(BigDecimal.ZERO)) {
+                notifyOrder(order.getId(), PayMethod.WEIXIN, null);
+            }
+            return order;
+        } catch (Exception e) {
+            collectionService.increaseStock(collectionId, qty);
+            throw e;
         }
-        return order;
     }
 
     public void payOrderAlipay(Long id, Model model) {
@@ -461,20 +474,20 @@ public class OrderService {
         if (order.getStatus() != OrderStatus.NOT_PAID) {
             throw new BusinessException("已支付订单无法取消");
         }
-        Collection collection = collectionRepo.findById(order.getCollectionId())
-                .orElseThrow(new BusinessException("藏品不存在"));
-        User minter = userRepo.findById(collection.getMinterId()).orElseThrow(new BusinessException("铸造者不存在"));
 
-        if (collection.getSource() == CollectionSource.TRANSFER) {
-            Asset asset = assetRepo.findById(collection.getAssetId()).orElse(null);
+        CollectionSource source = Optional.ofNullable(order.getSource()).orElseGet(() ->
+                collectionRepo.findById(order.getCollectionId()).map(Collection::getSource).orElse(null));
+
+        if (source == CollectionSource.TRANSFER) {
+            Asset asset = assetRepo.findById(order.getAssetId()).orElse(null);
             if (asset != null) {
                 asset.setStatus(AssetStatus.NORMAL);
                 assetRepo.save(asset);
             }
-            collectionRepo.setOnShelf(collection.getId(), true);
+            collectionRepo.setOnShelf(order.getCollectionId(), true);
         }
-        collectionRepo.increaseSale(collection.getId(), -order.getQty());
-        collectionRepo.increaseStock(collection.getId(), order.getQty());
+        collectionService.decreaseStock(order.getCollectionId(), order.getQty());
+        collectionService.increaseSale(order.getCollectionId(), order.getQty());
 
         order.setStatus(OrderStatus.CANCELLED);
         order.setCancelTime(LocalDateTime.now());

+ 62 - 0
src/main/java/com/izouma/nineth/service/RedisLock.java

@@ -0,0 +1,62 @@
+package com.izouma.nineth.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class RedisLock {
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 加锁
+     *
+     * @param key   productId - 商品的唯一标志
+     * @param value 当前时间+超时时间 也就是时间戳
+     * @return
+     */
+    public boolean lock(String key, String value) {
+        if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {//对应setnx命令
+            //可以成功设置,也就是key不存在
+            return true;
+        }
+
+        //判断锁超时 - 防止原来的操作异常,没有运行解锁操作  防止死锁
+        String currentValue = stringRedisTemplate.opsForValue().get(key);
+        //如果锁过期
+        if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {//currentValue不为空且小于当前时间
+            //获取上一个锁的时间value
+            String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);//对应getset,如果key存在
+
+            //假设两个线程同时进来这里,因为key被占用了,而且锁过期了。获取的值currentValue=A(get取的旧的值肯定是一样的),两个线程的value都是B,key都是K.锁时间已经过期了。
+            //而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的value已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
+            if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
+                //oldValue不为空且oldValue等于currentValue,也就是校验是不是上个对应的商品时间戳,也是防止并发
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    /**
+     * 解锁
+     *
+     * @param key
+     * @param value
+     */
+    public void unlock(String key, String value) {
+        try {
+            String currentValue = stringRedisTemplate.opsForValue().get(key);
+            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
+                stringRedisTemplate.opsForValue().getOperations().delete(key);//删除key
+            }
+        } catch (Exception e) {
+            log.error("[Redis分布式锁] 解锁出现异常了,{}", e);
+        }
+    }
+}

+ 9 - 0
src/main/java/com/izouma/nineth/web/OrderController.java

@@ -85,6 +85,15 @@ public class OrderController extends BaseController {
                 collectionId, qty, addressId, couponId, invitor);
     }
 
+    @PostMapping("/mqCreate")
+    public Order mqCreate(@RequestParam Long collectionId, @RequestParam int qty,
+                        @RequestParam(required = false) Long addressId,
+                        @RequestParam(required = false) Long couponId,
+                        @RequestParam(required = false) Long invitor) {
+        return orderService.create(SecurityUtils.getAuthenticatedUser().getId(),
+                collectionId, qty, addressId, couponId, invitor);
+    }
+
     @PostMapping("/hide")
     public void hide(@RequestParam Long id) {
         Order order = orderRepo.findById(id).orElseThrow(new BusinessException("订单不存在"));

+ 40 - 0
src/main/java/com/izouma/nineth/web/TestStockController.java

@@ -0,0 +1,40 @@
+package com.izouma.nineth.web;
+
+import com.izouma.nineth.service.RedisLock;
+import lombok.AllArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.PostConstruct;
+
+@RestController
+@RequestMapping("/teststock")
+@AllArgsConstructor
+public class TestStockController {
+
+    private RedisLock                     redisLock;
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @PostConstruct
+    public void init() {
+        redisTemplate.opsForValue().set("teststock", 1000);
+    }
+
+    @GetMapping("/setup")
+    public void setup() {
+        redisTemplate.opsForValue().getAndSet("teststock", 1000);
+    }
+
+    @GetMapping("/test")
+    public String test() {
+        int stock = Math.toIntExact(redisTemplate.opsForValue().decrement("teststock", 1));
+        if (stock < 0) {
+            redisTemplate.opsForValue().increment("teststock", 1);
+            return "库存不足";
+        } else {
+            return "ok";
+        }
+    }
+}

+ 14 - 4
src/main/resources/application.yaml

@@ -138,6 +138,10 @@ general:
   name: 绿洲宇宙
   org: 华储艺术品中心(深圳)有限公司
   short-name: 华储
+  create-order-group: create-order-group-dev
+  create-order-topic: create-order-topic-dev
+  update-stock-group: update-stock-group-dev
+  update-stock-topic: update-stock-topic-dev
 mychain:
   rest:
     bizid: a00e36c5
@@ -175,9 +179,7 @@ adapay:
 rocketmq:
   name-server: 120.24.204.226:9876
   producer:
-    group: raex-dev-producer-group
-  consumer:
-    group: raex-dev-consumer-group
+    group: my-producer-dev
 ---
 
 spring:
@@ -204,6 +206,10 @@ spring:
     password: jV%93RtjUx82Tp
 general:
   host: https://www.raex.vip
+  create-order-group: create-order-group
+  create-order-topic: create-order-topic
+  update-stock-group: update-stock-group
+  update-stock-topic: update-stock-topic
 wx:
   pay:
     notify-url: https://www.raex.vip/notify/order/weixin
@@ -213,4 +219,8 @@ alipay:
   notify-url: https://www.raex.vip/notify/order/alipay
   return-url: https://www.raex.vip/9th/home
 adapay:
-  notify-url: https://www.raex.vip/notify/adapay
+  notify-url: https://www.raex.vip/notify/adapay
+rocketmq:
+  name-server: 172.29.50.102:9876
+  producer:
+    group: my-producer

+ 0 - 5
src/test/java/com/izouma/nineth/service/AssetServiceTest.java

@@ -48,11 +48,6 @@ class AssetServiceTest extends ApplicationTests {
     void testCancelConsignment() {
     }
 
-    @Test
-    public void testLock() throws InterruptedException {
-        assetService.testLock("123", "ddd");
-    }
-
     @Test
     public void testCreateAsset() {
         Order order = orderRepo.findById(4618L).get();

+ 8 - 13
src/test/java/com/izouma/nineth/service/OrderServiceTest.java

@@ -11,12 +11,9 @@ import com.izouma.nineth.dto.UserBankCard;
 import com.izouma.nineth.enums.AssetStatus;
 import com.izouma.nineth.enums.AuthStatus;
 import com.izouma.nineth.enums.OrderStatus;
-import com.izouma.nineth.enums.PayMethod;
-import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.*;
 import com.izouma.nineth.utils.FileUtils;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.codec.EncoderException;
 import org.apache.commons.collections.MapUtils;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -50,9 +47,12 @@ public class OrderServiceTest extends ApplicationTests {
     private CollectionService collectionService;
 
     @Test
-    public void create() throws EncoderException, WxPayException {
-        Order order = orderService.create(1110L, 1777L, 1, null, 1896L, null);
-        assert order.getStatus() == OrderStatus.FINISH;
+    public void create() {
+        Order order = new Order();
+        order.setId(1234L);
+        orderRepo.save(order);
+
+        assert orderRepo.findById(1234L).orElse(null) != null;
     }
 
     @Test
@@ -115,8 +115,8 @@ public class OrderServiceTest extends ApplicationTests {
                 }
             }
             orderRepo.delete(errOrder);
-            collectionRepo.increaseStock(collection.getId(), 1);
-            collectionRepo.increaseSale(collection.getId(), -1);
+            collectionService.increaseStock(collection.getId(), 1);
+            collectionService.decreaseSale(collection.getId(), -1);
         }
     }
 
@@ -225,9 +225,4 @@ public class OrderServiceTest extends ApplicationTests {
         });
 
     }
-
-    @Test
-    public void notifyOrder() {
-        orderService.notifyOrder(923604613259591680L, PayMethod.ALIPAY, "11111");
-    }
 }