xiongzhu 4 tahun lalu
induk
melakukan
6790e6a03b

+ 8 - 22
src/main/java/com/izouma/nineth/domain/AirDrop.java

@@ -2,6 +2,7 @@ package com.izouma.nineth.domain;
 
 import com.izouma.nineth.converter.LongArrayConverter;
 import com.izouma.nineth.converter.StringArrayConverter;
+import com.izouma.nineth.enums.AirDropType;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
@@ -9,10 +10,7 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import javax.persistence.Column;
-import javax.persistence.Convert;
-import javax.persistence.Entity;
-import java.time.LocalDateTime;
+import javax.persistence.*;
 import java.util.List;
 
 @Data
@@ -30,23 +28,14 @@ public class AirDrop extends BaseEntity {
     private String remark;
 
     @ApiModelProperty("空投类型")
-    private String type;
+    @Enumerated(EnumType.STRING)
+    private AirDropType type;
 
-    @ApiModelProperty("兑换券名称")
-    private String couponName;
+    @ApiModelProperty("兑换券ID")
+    private Long couponId;
 
-    @ApiModelProperty("兑换券备注")
-    private String couponRemark;
-
-    private boolean couponLimited;
-
-    private boolean couponNeedGas;
-
-    @Convert(converter = LongArrayConverter.class)
-    @Column(columnDefinition = "TEXT")
-    private List<Long> couponCollectionIds;
-
-    private LocalDateTime couponExpiration;
+    @ApiModelProperty("藏品ID")
+    private Long collectionId;
 
     @Column(columnDefinition = "TEXT")
     @Convert(converter = StringArrayConverter.class)
@@ -58,9 +47,6 @@ public class AirDrop extends BaseEntity {
     @ApiModelProperty("用户ID")
     private List<Long> userIds;
 
-    @ApiModelProperty("藏品ID")
-    private Long collectionId;
-
     @Column(columnDefinition = "TEXT")
     private String result;
 }

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

@@ -141,4 +141,6 @@ public class Order extends BaseEntity {
     private Long assetId;
 
     private boolean hide;
+
+    private Long couponId;
 }

+ 2 - 1
src/main/java/com/izouma/nineth/domain/PrivilegeOption.java

@@ -5,9 +5,9 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
+import javax.persistence.Column;
 import javax.persistence.Convert;
 import javax.persistence.Entity;
-import java.time.LocalDateTime;
 import java.util.List;
 
 @Data
@@ -23,6 +23,7 @@ public class PrivilegeOption extends BaseEntity {
     private String type;
 
     @Convert(converter = StringArrayConverter.class)
+    @Column(columnDefinition = "TEXT")
     private List<String> icon;
 
     private boolean once;

+ 7 - 0
src/main/java/com/izouma/nineth/domain/UserCoupon.java

@@ -21,6 +21,7 @@ import java.util.List;
 @Builder
 @ApiModel("兑换券")
 public class UserCoupon extends BaseEntity {
+
     private Long userId;
 
     private Long couponId;
@@ -31,18 +32,24 @@ public class UserCoupon extends BaseEntity {
     @ApiModelProperty("备注")
     private String remark;
 
+    @ApiModelProperty("是否指定藏品可用")
     private boolean limited;
 
+    @ApiModelProperty("需要支付GAS")
     private boolean needGas;
 
     @Convert(converter = LongArrayConverter.class)
     @Column(columnDefinition = "TEXT")
+    @ApiModelProperty("可用藏品ID")
     private List<Long> collectionIds;
 
+    @ApiModelProperty("过期时间")
     private LocalDateTime expiration;
 
+    @ApiModelProperty("是否已使用")
     private boolean used;
 
+    @ApiModelProperty("使用时间")
     private LocalDateTime useTime;
 
 }

+ 17 - 0
src/main/java/com/izouma/nineth/enums/AirDropType.java

@@ -0,0 +1,17 @@
+package com.izouma.nineth.enums;
+
+public enum AirDropType {
+    asset("藏品"),
+    coupon("兑换券"),
+    ;
+
+    private final String description;
+
+    AirDropType(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

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

@@ -1,20 +1,56 @@
 package com.izouma.nineth.service;
 
 import com.izouma.nineth.domain.AirDrop;
+import com.izouma.nineth.domain.Collection;
+import com.izouma.nineth.domain.Coupon;
+import com.izouma.nineth.domain.UserCoupon;
 import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.enums.AirDropType;
+import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.AirDropRepo;
+import com.izouma.nineth.repo.CollectionRepo;
+import com.izouma.nineth.repo.CouponRepo;
+import com.izouma.nineth.repo.UserCouponRepo;
 import com.izouma.nineth.utils.JpaUtils;
 import lombok.AllArgsConstructor;
+import org.springframework.beans.BeanUtils;
 import org.springframework.data.domain.Page;
 import org.springframework.stereotype.Service;
 
+import javax.transaction.Transactional;
+
 @Service
 @AllArgsConstructor
 public class AirDropService {
 
-    private AirDropRepo airDropRepo;
+    private AirDropRepo    airDropRepo;
+    private CouponRepo     couponRepo;
+    private UserCouponRepo userCouponRepo;
+    private CollectionRepo collectionRepo;
 
     public Page<AirDrop> all(PageQuery pageQuery) {
         return airDropRepo.findAll(JpaUtils.toSpecification(pageQuery, AirDrop.class), JpaUtils.toPageRequest(pageQuery));
     }
+
+    @Transactional
+    public AirDrop create(AirDrop record) {
+        if (AirDropType.coupon == record.getType()) {
+            Coupon coupon = couponRepo.findById(record.getCouponId()).orElseThrow(new BusinessException("兑换券不存在"));
+            record.getUserIds().stream().parallel().forEach(userId -> {
+                UserCoupon userCoupon = new UserCoupon();
+                BeanUtils.copyProperties(coupon, userCoupon);
+                userCoupon.setId(null);
+                userCoupon.setCouponId(coupon.getId());
+                userCoupon.setUserId(userId);
+                userCouponRepo.save(userCoupon);
+            });
+        } else {
+            Collection collection = collectionRepo.findById(record.getCollectionId())
+                    .orElseThrow(new BusinessException("藏品不存在"));
+            if (collection.getStock() < record.getUserIds().size()) {
+                throw new BusinessException("藏品库存不足");
+            }
+        }
+        return airDropRepo.save(record);
+    }
 }

+ 56 - 5
src/main/java/com/izouma/nineth/service/CollectionService.java

@@ -4,7 +4,6 @@ import com.izouma.nineth.domain.*;
 import com.izouma.nineth.dto.CollectionDTO;
 import com.izouma.nineth.dto.CreateBlindBox;
 import com.izouma.nineth.dto.PageQuery;
-import com.izouma.nineth.dto.UserDTO;
 import com.izouma.nineth.enums.CollectionType;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.*;
@@ -12,6 +11,8 @@ import com.izouma.nineth.utils.JpaUtils;
 import com.izouma.nineth.utils.SecurityUtils;
 import lombok.AllArgsConstructor;
 import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.RandomUtils;
+import org.apache.commons.lang3.Range;
 import org.springframework.beans.BeanUtils;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
@@ -21,15 +22,13 @@ import org.springframework.data.jpa.domain.Specification;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
 import javax.persistence.criteria.Predicate;
-import javax.persistence.criteria.Root;
 import javax.transaction.Transactional;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 @Service
@@ -193,4 +192,56 @@ public class CollectionService {
         }
         collectionRepo.saveAll(collections);
     }
+
+
+    public BlindBoxItem draw(Long collectionId) {
+        List<BlindBoxItem> items = blindBoxItemRepo.findByBlindBoxId(collectionId);
+
+        Map<BlindBoxItem, Range<Integer>> randomRange = new HashMap<>();
+        int c = 0, sum = 0;
+        for (BlindBoxItem item : items) {
+            randomRange.put(item, Range.between(c, c + item.getStock()));
+            c += item.getStock();
+            sum += item.getStock();
+        }
+
+        int retry = 0;
+        BlindBoxItem winItem = null;
+        while (winItem == null) {
+            retry++;
+            int rand = RandomUtils.nextInt(0, sum + 1);
+            for (Map.Entry<BlindBoxItem, Range<Integer>> entry : randomRange.entrySet()) {
+                BlindBoxItem item = entry.getKey();
+                Range<Integer> range = entry.getValue();
+                if (rand >= range.getMinimum() && rand < range.getMaximum()) {
+                    int total = items.stream().filter(i -> !i.isRare())
+                            .mapToInt(BlindBoxItem::getTotal).sum();
+                    int stock = items.stream().filter(i -> !i.isRare())
+                            .mapToInt(BlindBoxItem::getStock).sum();
+                    if (item.isRare()) {
+                        double nRate = stock / (double) total;
+                        double rRate = (item.getStock() - 1) / (double) item.getTotal();
+
+                        if (Math.abs(nRate - rRate) < (1 / (double) item.getTotal()) || retry > 1 || rRate == 0) {
+                            if (!(nRate > 0.1 && item.getStock() == 1)) {
+                                winItem = item;
+                            }
+                        }
+                    } else {
+                        double nRate = (stock - 1) / (double) total;
+                        double rRate = item.getStock() / (double) item.getTotal();
+
+                        if (Math.abs(nRate - rRate) < 0.2 || retry > 1 || nRate == 0) {
+                            winItem = item;
+                        }
+                    }
+                }
+            }
+
+            if (retry > 100 && winItem == null) {
+                throw new BusinessException("盲盒抽卡失败");
+            }
+        }
+        return winItem;
+    }
 }

+ 36 - 3
src/main/java/com/izouma/nineth/service/OrderService.java

@@ -57,17 +57,28 @@ public class OrderService {
     private SysConfigService sysConfigService;
     private BlindBoxItemRepo blindBoxItemRepo;
     private AssetRepo        assetRepo;
+    private UserCouponRepo   userCouponRepo;
 
     public Page<Order> all(PageQuery pageQuery) {
         return orderRepo.findAll(JpaUtils.toSpecification(pageQuery, Order.class), JpaUtils.toPageRequest(pageQuery));
     }
 
     @Transactional
-    public Order create(Long userId, Long collectionId, int qty, Long addressId) {
+    public Order create(Long userId, Long collectionId, int qty, Long addressId, Long userCouponId) {
         if (qty <= 0) throw new BusinessException("数量必须大于0");
         User user = userRepo.findByIdAndDelFalse(userId).orElseThrow(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.isOnShelf()) {
             throw new BusinessException("藏品已下架");
@@ -121,7 +132,17 @@ public class OrderService {
                         .orElse(null))
                 .status(OrderStatus.NOT_PAID)
                 .assetId(collection.getAssetId())
+                .couponId(userCouponId)
                 .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("资产不存在"));
@@ -130,7 +151,11 @@ public class OrderService {
             collection.setOnShelf(false);
             collectionRepo.save(collection);
         }
-        return orderRepo.save(order);
+        order = orderRepo.save(order);
+        if (order.getTotalPrice().equals(BigDecimal.ZERO)) {
+            notifyOrder(order.getId(), PayMethod.WEIXIN, null);
+        }
+        return order;
     }
 
     public void payOrderAlipay(Long id, Model model) {
@@ -344,13 +369,21 @@ public class OrderService {
         order.setStatus(OrderStatus.CANCELLED);
         order.setCancelTime(LocalDateTime.now());
         orderRepo.save(order);
+
+        if (order.getCouponId() != null) {
+            userCouponRepo.findById(order.getCouponId()).ifPresent(coupon -> {
+                coupon.setUsed(false);
+                coupon.setUseTime(null);
+                userCouponRepo.save(coupon);
+            });
+        }
     }
 
     @Scheduled(fixedRate = 60000)
     public void batchCancel() {
         List<Order> orders = orderRepo.findByStatusAndCreatedAtBeforeAndDelFalse(OrderStatus.NOT_PAID,
                 LocalDateTime.now().minusMinutes(5));
-        orders.stream().forEach(this::cancel);
+        orders.forEach(this::cancel);
     }
 
     public void refundCancelled(Order order) {

+ 4 - 9
src/main/java/com/izouma/nineth/web/AirDropController.java

@@ -1,10 +1,10 @@
 package com.izouma.nineth.web;
+
 import com.izouma.nineth.domain.AirDrop;
-import com.izouma.nineth.service.AirDropService;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.AirDropRepo;
-import com.izouma.nineth.utils.ObjUtils;
+import com.izouma.nineth.service.AirDropService;
 import com.izouma.nineth.utils.excel.ExcelUtils;
 import lombok.AllArgsConstructor;
 import org.springframework.data.domain.Page;
@@ -22,15 +22,10 @@ public class AirDropController extends BaseController {
     private AirDropService airDropService;
     private AirDropRepo airDropRepo;
 
-    //@PreAuthorize("hasRole('ADMIN')")
+    @PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/save")
     public AirDrop save(@RequestBody AirDrop record) {
-        if (record.getId() != null) {
-            AirDrop orig = airDropRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
-            ObjUtils.merge(orig, record);
-            return airDropRepo.save(orig);
-        }
-        return airDropRepo.save(record);
+        return airDropService.create(record);
     }
 
 

+ 3 - 2
src/main/java/com/izouma/nineth/web/OrderController.java

@@ -73,8 +73,9 @@ public class OrderController extends BaseController {
     }
 
     @PostMapping("/create")
-    public Order create(@RequestParam Long collectionId, @RequestParam int qty, @RequestParam(required = false) Long addressId) {
-        return orderService.create(SecurityUtils.getAuthenticatedUser().getId(), collectionId, qty, addressId);
+    public Order create(@RequestParam Long collectionId, @RequestParam int qty, @RequestParam(required = false) Long addressId, @RequestParam(required = false) Long couponId) {
+        return orderService.create(SecurityUtils.getAuthenticatedUser().getId(),
+                collectionId, qty, addressId, couponId);
     }
 
     @GetMapping("/createAsset")

+ 3 - 3
src/main/java/com/izouma/nineth/web/UserCouponController.java

@@ -1,14 +1,14 @@
 package com.izouma.nineth.web;
+
 import com.izouma.nineth.domain.UserCoupon;
-import com.izouma.nineth.service.UserCouponService;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.UserCouponRepo;
+import com.izouma.nineth.service.UserCouponService;
 import com.izouma.nineth.utils.ObjUtils;
 import com.izouma.nineth.utils.excel.ExcelUtils;
 import lombok.AllArgsConstructor;
 import org.springframework.data.domain.Page;
-import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletResponse;
@@ -20,7 +20,7 @@ import java.util.List;
 @AllArgsConstructor
 public class UserCouponController extends BaseController {
     private UserCouponService userCouponService;
-    private UserCouponRepo userCouponRepo;
+    private UserCouponRepo    userCouponRepo;
 
     //@PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/save")

+ 20 - 6
src/main/vue/src/views/AirDropEdit.vue

@@ -34,7 +34,15 @@
                         </el-select>
                     </el-form-item>
                     <el-form-item prop="couponId" label="兑换券" v-if="formData.type === 'coupon'">
-                        <el-input v-model="formData.couponId"></el-input>
+                        <el-select v-model="formData.couponId" filterable>
+                            <el-option v-for="item in coupons" :label="item.name" :value="item.id" :key="item.id">
+                                <span style="float: left">{{ item.name }}</span>
+                                <span style="float: right; color: #8492a6; font-size: 13px">#{{ item.id }}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item prop="collectionId" label="藏品" v-else>
+                        <collection-search v-model="formData.collectionId"></collection-search>
                     </el-form-item>
                     <el-form-item prop="userIds" label="空投对象">
                         <el-table :data="users">
@@ -49,7 +57,6 @@
                         </el-table>
                         <el-button @click="addUser" size="mini">添加</el-button>
                     </el-form-item>
-                    <el-form-item prop="result" label="result"> </el-form-item>
                     <el-form-item class="form-submit">
                         <el-button @click="onSave" :loading="saving" type="primary"> 保存 </el-button>
                         <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
@@ -64,7 +71,7 @@
         <el-dialog
             :visible.sync="showPhoneDialog"
             title="添加空投对象"
-            width="500px"
+            width="600px"
             :close-on-click-modal="false"
             :show-close="!searchingPhone"
         >
@@ -73,7 +80,7 @@
                     <el-input
                         v-model="phone"
                         type="textarea"
-                        :autosize="{ minRows: 3 }"
+                        :autosize="{ minRows: 3, maxRows: 20 }"
                         placeholder="可输入多个手机号,用空格或回车分隔"
                     ></el-input>
                 </el-form-item>
@@ -100,6 +107,11 @@ export default {
                     this.$message.error(e.error);
                 });
         }
+        this.$http
+            .post('/coupon/all', { size: 10000, query: { del: false }, sort: 'createdAt,desc' }, { body: 'json' })
+            .then(res => {
+                this.coupons = res.content;
+            });
     },
     data() {
         return {
@@ -147,7 +159,8 @@ export default {
                         trigger: 'blur'
                     }
                 ],
-                couponExpiration: [{ required: true, message: '请输入兑换券过期时间' }]
+                couponExpiration: [{ required: true, message: '请输入兑换券过期时间' }],
+                collectionId: [{ required: true, message: '请选择藏品' }]
             },
             typeOptions: [
                 { label: '藏品', value: 'asset' },
@@ -156,7 +169,8 @@ export default {
             phone: '',
             showPhoneDialog: false,
             searchingPhone: false,
-            users: []
+            users: [],
+            coupons: []
         };
     },
     methods: {

+ 4 - 2
src/main/vue/src/views/AirDropList.vue

@@ -40,8 +40,10 @@
             <el-table-column prop="id" label="ID" width="100"> </el-table-column>
             <el-table-column prop="name" label="空投名称"> </el-table-column>
             <el-table-column prop="type" label="空投类型" :formatter="typeFormatter"> </el-table-column>
-            <el-table-column prop="couponName" label="兑换券名称"> </el-table-column>
-            <el-table-column prop="phone" label="手机号"> </el-table-column>
+            <el-table-column prop="phone" label="手机号" show-overflow-tooltip>
+                <template v-slot="{ row }">{{ row.phone.join() }}</template>
+            </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间"></el-table-column>
             <!-- <el-table-column label="操作" align="center" fixed="right" width="150">
                 <template slot-scope="{ row }">
                     <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>

+ 9 - 0
src/main/vue/src/views/CouponEdit.vue

@@ -213,6 +213,15 @@ export default {
                 })
                 .catch(() => {});
         }
+    },
+    watch: {
+        collections(val) {
+            this.$set(
+                this.formData,
+                'collectionIds',
+                val.map(i => i.id)
+            );
+        }
     }
 };
 </script>

+ 3 - 4
src/test/java/com/izouma/nineth/service/OrderServiceTest.java

@@ -1,9 +1,9 @@
 package com.izouma.nineth.service;
 
-import com.github.binarywang.wxpay.constant.WxPayConstants;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.izouma.nineth.ApplicationTests;
 import com.izouma.nineth.domain.Order;
+import com.izouma.nineth.enums.OrderStatus;
 import org.apache.commons.codec.EncoderException;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -14,8 +14,7 @@ public class OrderServiceTest extends ApplicationTests {
 
     @Test
     public void create() throws EncoderException, WxPayException {
-        Order order = orderService.create(30L, 201L, 1, null);
-        String url = (String) orderService.payOrderWeixin(order.getId(), WxPayConstants.TradeType.MWEB, null);
-        System.out.println(url);
+        Order order = orderService.create(1110L, 1777L, 1, null, 1896L);
+        assert order.getStatus() == OrderStatus.FINISH;
     }
 }