Browse Source

自动收购

xiongzhu 2 năm trước cách đây
mục cha
commit
b6f83296a8

+ 11 - 5
pom.xml

@@ -114,10 +114,10 @@
             <optional>true</optional>
         </dependency>
 
-<!--        <dependency>-->
-<!--            <groupId>org.springframework.boot</groupId>-->
-<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
-<!--        </dependency>-->
+        <!--        <dependency>-->
+        <!--            <groupId>org.springframework.boot</groupId>-->
+        <!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
+        <!--        </dependency>-->
 
         <dependency>
             <groupId>org.apache.commons</groupId>
@@ -318,7 +318,6 @@
         </dependency>
 
 
-
         <dependency>
             <groupId>org.hibernate</groupId>
             <artifactId>hibernate-envers</artifactId>
@@ -379,6 +378,13 @@
             <artifactId>telegrambots-abilities</artifactId>
             <version>6.5.0</version>
         </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-resolver-dns-native-macos</artifactId>
+            <version>4.1.72.Final</version>
+            <classifier>osx-aarch_64</classifier>
+        </dependency>
     </dependencies>
 
 </project>

+ 2 - 0
src/main/java/com/izouma/awesomeAdmin/Application.java

@@ -5,6 +5,7 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import org.telegram.telegrambots.meta.TelegramBotsApi;
 import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
@@ -14,6 +15,7 @@ import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;
 @EnableJpaAuditing
 @EnableCaching
 @EnableScheduling
+@EnableAsync
 public class Application {
 
     public static void main(String[] args) {

+ 2 - 0
src/main/java/com/izouma/awesomeAdmin/domain/Order.java

@@ -136,4 +136,6 @@ public class Order extends BaseEntity {
     private boolean del;
 
     private Long batchId;
+
+    private boolean autoCreated;
 }

+ 2 - 3
src/main/java/com/izouma/awesomeAdmin/domain/UserVip.java

@@ -1,7 +1,6 @@
 package com.izouma.awesomeAdmin.domain;
 
 import com.izouma.awesomeAdmin.annotations.Searchable;
-import com.izouma.awesomeAdmin.enums.VipStatus;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -46,10 +45,10 @@ public class UserVip extends BaseEntity {
     private boolean autoTradeEnabled;
 
     @ApiModelProperty("每天购买次数")
-    private int dailyFrequency;
+    private int dailyLimit;
 
     @ApiModelProperty("剩余购买次数")
-    private int leftFrequency;
+    private int todayUsed;
 
     @Formula(value = "if(expire_at < now(), 1, 0)")
     @ApiModelProperty("是否过期")

+ 1 - 2
src/main/java/com/izouma/awesomeAdmin/domain/VipType.java

@@ -6,7 +6,6 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Table;
 
@@ -31,5 +30,5 @@ public class VipType extends BaseEntity {
     private boolean enabled;
 
     @ApiModelProperty("每天购买次数")
-    private int dailyFrequency;
+    private int dailyLimit;
 }

+ 16 - 1
src/main/java/com/izouma/awesomeAdmin/repo/UserVipRepo.java

@@ -3,9 +3,11 @@ package com.izouma.awesomeAdmin.repo;
 import com.izouma.awesomeAdmin.domain.UserVip;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
 import org.springframework.data.jpa.repository.Query;
 
-import java.util.Optional;
+import java.math.BigDecimal;
+import java.util.List;
 
 public interface UserVipRepo extends JpaRepository<UserVip, Long>, JpaSpecificationExecutor<UserVip> {
 
@@ -13,4 +15,17 @@ public interface UserVipRepo extends JpaRepository<UserVip, Long>, JpaSpecificat
 
     @Query(value = "select ifnull(min(u.weight), 0) from user_vip u", nativeQuery = true)
     long minWeight();
+
+    @Query("select u from UserVip u " +
+            "join UserBalance b on u.userId = b.userId " +
+            "where u.expired = false and u.autoTradeEnabled = true and u.todayUsed < u.dailyLimit and b.balance >= ?1 and b.userId <> ?2")
+    List<UserVip> findMatchVip(BigDecimal price, Long userId);
+
+    @Query("update UserVip u set u.todayUsed = u.todayUsed + 1 where u.userId = ?1")
+    @Modifying
+    void increaseUsed(Long userId);
+
+    @Query("update UserVip u set u.todayUsed = 0 where u.expired = false")
+    @Modifying
+    void resetUsedLimit();
 }

+ 4 - 2
src/main/java/com/izouma/awesomeAdmin/service/OrderService.java

@@ -149,7 +149,7 @@ public class OrderService {
 
     @Transactional
     @RedisLock(value = "'createOrderProduct::'+#productId", behavior = RedisLock.Behavior.WAIT)
-    public Order createOrder(Long userId, Long productId, Long addressId) {
+    public Order createOrder(Long userId, Long productId, Long addressId, boolean vip) {
         SecurityUtils.checkBanned();
         if (!sysConfigService.getBoolean(Constants.SYSTEM_OPEN)) {
             throw new BusinessException(Translator.toLocale("system.closed"));
@@ -179,7 +179,7 @@ public class OrderService {
         } else if (LocalTime.now().isAfter(saleBatch.getSaleEnd())) {
             throw new BusinessException(Translator.toLocale("sale.ended"));
         }
-        if (saleBatch.getDailyLimit() > 0) {
+        if (!vip && saleBatch.getDailyLimit() > 0) {
             LocalDate now = LocalDate.now();
             long num = orderRepo.countByUserIdAndBatchIdAndCreatedAtBetweenAndStatusNotIn(userId, product.getBatchId(),
                     now.atStartOfDay(), now.atTime(LocalTime.MAX), Collections.singletonList(CANCELED));
@@ -230,6 +230,7 @@ public class OrderService {
                              .status(OrderStatus.NOT_PAID)
                              .rated(false)
                              .batchId(product.getBatchId())
+                             .autoCreated(vip)
                              .build();
                 order = orderRepo.save(order);
             } else {
@@ -263,6 +264,7 @@ public class OrderService {
                              .status(OrderStatus.NOT_PAID)
                              .rated(false)
                              .batchId(product.getBatchId())
+                             .autoCreated(vip)
                              .build();
                 order = orderRepo.save(order);
                 delegation.setBuyerOrderId(order.getId());

+ 9 - 1
src/main/java/com/izouma/awesomeAdmin/service/UserVipService.java

@@ -6,8 +6,8 @@ import com.izouma.awesomeAdmin.repo.UserVipRepo;
 import com.izouma.awesomeAdmin.utils.Translator;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
-import org.springframework.web.bind.annotation.RequestParam;
 
 import java.math.BigDecimal;
 
@@ -27,6 +27,7 @@ public class UserVipService {
         userVip.setMinPrice(minPrice);
         userVip.setMaxPrice(maxPrice);
         userVipRepo.save(userVip);
+        log.info("auto trade enabled for user {}", userId);
     }
 
     public void disableAutoTrade(Long userId) {
@@ -36,5 +37,12 @@ public class UserVipService {
         }
         userVip.setAutoTradeEnabled(false);
         userVipRepo.save(userVip);
+        log.info("auto trade disabled for user {}", userId);
+    }
+
+    @Scheduled(cron = "10 0 0 * * ?")
+    public void resetUsedLimit() {
+        userVipRepo.resetUsedLimit();
+        log.info("all vip daily used limit are reset");
     }
 }

+ 54 - 0
src/main/java/com/izouma/awesomeAdmin/service/VipAsyncService.java

@@ -0,0 +1,54 @@
+package com.izouma.awesomeAdmin.service;
+
+import com.izouma.awesomeAdmin.domain.Order;
+import com.izouma.awesomeAdmin.domain.Product;
+import com.izouma.awesomeAdmin.domain.UserVip;
+import com.izouma.awesomeAdmin.exception.BusinessException;
+import com.izouma.awesomeAdmin.repo.UserVipRepo;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.AsyncResult;
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+public class VipAsyncService {
+    private final OrderService      orderService;
+    private final DelegationService delegationService;
+    private final UserVipRepo       userVipRepo;
+
+    @Async
+    @Transactional
+    public Future<Void> autoTrade(UserVip userVip, Product product) {
+        Long userId = userVip.getUserId();
+        try {
+            log.info("🚚 start, userId: {}, productId: {}", userId, product.getId());
+            Order order = orderService.createOrder(userId, product.getId(), null, true);
+            log.info("🚚 create order success, userId: {}, productId: {}, orderId: {}", userId, product.getId(), order.getId());
+
+            log.info("🚚 pay start, userId: {}, productId: {}", userId, product.getId());
+            orderService.balancePay(userId, order.getId());
+            log.info("🚚 pay success, userId: {}, productId: {}", userId, product.getId());
+
+            userVipRepo.increaseUsed(userId);
+            log.info("🚚 increase used limit, userId: {}, left: {}", userId, userVip.getDailyLimit() - userVip.getTodayUsed() - 1);
+
+            log.info("🚚 pay delegation start, userId: {}, productId: {}", userId, product.getId());
+            delegationService.payDelegationBalance(userId, order.getId());
+            log.info("🚚 pay delegation success, userId: {}, productId: {}", userId, product.getId());
+        } catch (Exception e) {
+            if (e instanceof BusinessException) {
+                log.error("🚚 error, message: {}", e.getMessage());
+            } else {
+                log.error("🚚 error", e);
+            }
+        }
+        return new AsyncResult<>(null);
+    }
+}

+ 39 - 16
src/main/java/com/izouma/awesomeAdmin/service/VipService.java

@@ -8,21 +8,21 @@ import com.izouma.awesomeAdmin.repo.UserVipRepo;
 import com.izouma.awesomeAdmin.repo.VipConfigRepo;
 import com.izouma.awesomeAdmin.repo.VipPurchaseRepo;
 import com.izouma.awesomeAdmin.repo.VipTypeRepo;
-import com.izouma.awesomeAdmin.utils.ObjUtils;
 import com.izouma.awesomeAdmin.utils.Translator;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
 import javax.transaction.Transactional;
 import java.math.BigDecimal;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
+import java.util.*;
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
@@ -38,6 +38,8 @@ public class VipService {
     private VipPurchaseRepo    vipPurchaseRepo;
     private OrderService       orderService;
     private ProductService     productService;
+    private DelegationService  delegationService;
+    private VipAsyncService    vipAsyncService;
 
     @Transactional
     public void purchaseVip(Long userId, Long vipTypeId, Long vipConfigId, int num) {
@@ -61,16 +63,17 @@ public class VipService {
                              .typeId(vipTypeId)
                              .autoTradeEnabled(false)
                              .startAt(LocalDateTime.now())
-                             .dailyFrequency(vipType.getDailyFrequency())
-                             .leftFrequency(vipType.getDailyFrequency())
+                             .dailyLimit(vipType.getDailyLimit())
+                             .todayUsed(0)
                              .weight(userVipRepo.minWeight())
                              .build();
         } else if (userVip.isExpired()) {
             userVip.setStartAt(LocalDateTime.now());
             userVip.setTypeId(vipTypeId);
             userVip.setAutoTradeEnabled(false);
-            userVip.setDailyFrequency(vipType.getDailyFrequency());
-            userVip.setLeftFrequency(vipType.getDailyFrequency());
+            userVip.setDailyLimit(vipType.getDailyLimit());
+            userVip.setTodayUsed(userVip.getExpireAt().toLocalDate().isEqual(LocalDate.now())
+                    ? userVip.getTodayUsed() : 0);
             userVip.setWeight(userVipRepo.minWeight());
         }
         BigDecimal totalPrice = vipConfig.getPrice().multiply(BigDecimal.valueOf(num));
@@ -93,13 +96,33 @@ public class VipService {
                                         .build());
     }
 
-    @Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
-    public void scheduleVip() {
-        List<Product> list = productService.list(null, 11548L, Arrays.asList(ProductStatus.IN_STOCK), Pageable.ofSize(100))
-                                           .getContent().stream().filter(product -> product.getDelayTo() == null
-                        || product.getDelayTo().isBefore(LocalDateTime.now()))
+    @Scheduled(fixedRate = 10, timeUnit = TimeUnit.SECONDS)
+    public synchronized void scheduleVip() {
+        log.info("🚚 schedule");
+        List<Product> list = productService.list(null, 11548L, Collections.singletonList(ProductStatus.IN_STOCK),
+                                                   PageRequest.ofSize(100).withSort(Sort.by("modifiedAt").descending()))
+                                           .getContent().stream()
+                                           .filter(product -> product.getDelayTo() == null
+                                                   || product.getDelayTo().isBefore(LocalDateTime.now()))
                                            .collect(Collectors.toList());
-
+        if (list.size() <= 10) {
+            log.info("🚚 schedule, no product");
+            return;
+        }
+        for (int i = 10; i < list.size(); i++) {
+            Product product = list.get(i);
+            List<UserVip> vips = userVipRepo.findMatchVip(product.getCurrentPrice(), product.getUserId());
+            if (vips.isEmpty()) {
+                log.info("🚚 no vip match, productId: {}, price: {}", product.getId(), product.getCurrentPrice());
+                continue;
+            }
+            UserVip chosenVip = vips.get(new Random().nextInt(vips.size()));
+            Future result = vipAsyncService.autoTrade(chosenVip, product);
+            try {
+                result.get();
+            } catch (Exception e) {
+                log.error("🚚 auto trade error", e);
+            }
+        }
     }
-
 }

+ 1 - 1
src/main/java/com/izouma/awesomeAdmin/web/OrderController.java

@@ -70,7 +70,7 @@ public class OrderController extends BaseController {
 
     @PostMapping("/createOrder")
     public Order createOrder(@RequestParam Long productId, @RequestParam(required = false) Long addressId) {
-        return orderService.createOrder(SecurityUtils.getAuthenticatedUser().getId(), productId, addressId);
+        return orderService.createOrder(SecurityUtils.getAuthenticatedUser().getId(), productId, addressId, false);
     }
 
     @PostMapping("/cancel/{orderId}")

+ 8 - 8
src/main/resources/logback-spring.xml

@@ -11,14 +11,14 @@
         <root level="INFO">
             <appender-ref ref="CONSOLE"/>
         </root>
-        <logger name="org.hibernate.SQL" level="DEBUG"/>
-        <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
-        <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG"/>
-        <logger name="org.springframework.jdbc.core.StatementCreatorUtils" level="TRACE"/>
-        <logger name="cn.binarywang.wx.miniapp" level="DEBUG"/>
-        <logger name="io.swagger.models.parameters.AbstractSerializableParameter" level="ERROR"/>
-        <logger name="org.freemarker" level="DEBUG"/>
-        <logger name="org.springframework.cache" level="TRACE"/>
+<!--        <logger name="org.hibernate.SQL" level="DEBUG"/>-->
+<!--        <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>-->
+<!--        <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG"/>-->
+<!--        <logger name="org.springframework.jdbc.core.StatementCreatorUtils" level="TRACE"/>-->
+<!--        <logger name="cn.binarywang.wx.miniapp" level="DEBUG"/>-->
+<!--        <logger name="io.swagger.models.parameters.AbstractSerializableParameter" level="ERROR"/>-->
+<!--        <logger name="org.freemarker" level="DEBUG"/>-->
+<!--        <logger name="org.springframework.cache" level="TRACE"/>-->
     </springProfile>
 
     <springProfile name="test">

+ 4 - 4
src/main/vue-admin/src/views/VipConfig.vue

@@ -11,7 +11,7 @@
                 <div class="intro">
                     {{ vipType.info }}
                     <br />
-                    <div>日购额度: {{ vipType.dailyFrequency }}</div>
+                    <div>日购额度: {{ vipType.dailyLimit }}</div>
                 </div>
             </div>
             <div class="config-item" v-for="vipConfig in vipType.configs" :key="vipConfig.id">
@@ -37,8 +37,8 @@
             <el-form-item prop="enabled" label="启用">
                 <el-switch v-model="vipTypeForm.enabled"></el-switch>
             </el-form-item>
-            <el-form-item prop="dailyFrequency" label="日购额度">
-                <el-input-number v-model="vipTypeForm.dailyFrequency"></el-input-number>
+            <el-form-item prop="dailyLimit" label="日购额度">
+                <el-input-number v-model="vipTypeForm.dailyLimit"></el-input-number>
             </el-form-item>
         </el-form>
         <template #footer>
@@ -79,7 +79,7 @@ export default {
                 pic: [{ required: true, message: '请上传图片', trigger: 'blur' }],
                 info: [{ required: true, message: '请输入简介', trigger: 'blur' }],
                 enabled: [{ required: true, message: '请选择启用', trigger: 'blur' }],
-                dailyFrequency: [{ required: true, message: '请输入日购额度', trigger: 'blur' }]
+                dailyLimit: [{ required: true, message: '请输入日购额度', trigger: 'blur' }]
             },
             savingType: false,
             showConfigDialog: false,