Explorar o código

Merge branch 'dev'

xiongzhu %!s(int64=4) %!d(string=hai) anos
pai
achega
647c7dc7bc
Modificáronse 38 ficheiros con 877 adicións e 113 borrados
  1. 5 0
      src/main/java/com/izouma/nineth/config/EventNames.java
  2. 4 0
      src/main/java/com/izouma/nineth/config/GeneralProperties.java
  3. 2 0
      src/main/java/com/izouma/nineth/config/RedisKeys.java
  4. 11 0
      src/main/java/com/izouma/nineth/domain/Collection.java
  5. 4 0
      src/main/java/com/izouma/nineth/domain/Order.java
  6. 29 0
      src/main/java/com/izouma/nineth/domain/PointRecord.java
  7. 7 1
      src/main/java/com/izouma/nineth/domain/User.java
  8. 17 0
      src/main/java/com/izouma/nineth/dto/adapay/DivMembersItem.java
  9. 88 0
      src/main/java/com/izouma/nineth/dto/adapay/PaymentItem.java
  10. 28 0
      src/main/java/com/izouma/nineth/dto/adapay/PaymentList.java
  11. 0 1
      src/main/java/com/izouma/nineth/event/CreateOrderEvent.java
  12. 43 0
      src/main/java/com/izouma/nineth/listener/BroadcastEventListener.java
  13. 1 1
      src/main/java/com/izouma/nineth/listener/CreateOrderListener.java
  14. 28 0
      src/main/java/com/izouma/nineth/listener/UpdateQuotaListener.java
  15. 10 0
      src/main/java/com/izouma/nineth/repo/AssetRepo.java
  16. 26 2
      src/main/java/com/izouma/nineth/repo/CollectionRepo.java
  17. 8 0
      src/main/java/com/izouma/nineth/repo/PointRecordRepo.java
  18. 7 1
      src/main/java/com/izouma/nineth/repo/UserRepo.java
  19. 44 13
      src/main/java/com/izouma/nineth/service/AdapayMerchantService.java
  20. 12 1
      src/main/java/com/izouma/nineth/service/AssetMintService.java
  21. 37 1
      src/main/java/com/izouma/nineth/service/AssetService.java
  22. 25 2
      src/main/java/com/izouma/nineth/service/CollectionService.java
  23. 32 8
      src/main/java/com/izouma/nineth/service/OrderService.java
  24. 32 8
      src/main/java/com/izouma/nineth/service/UserService.java
  25. 1 1
      src/main/java/com/izouma/nineth/web/AdapayMerchantController.java
  26. 15 0
      src/main/java/com/izouma/nineth/web/AssetController.java
  27. 1 1
      src/main/java/com/izouma/nineth/web/OrderController.java
  28. 22 0
      src/main/resources/application.yaml
  29. BIN=BIN
      src/main/vue/public/favicon.ico
  30. 2 2
      src/main/vue/public/index.html
  31. 109 54
      src/main/vue/src/views/BlindBoxEdit.vue
  32. 109 11
      src/main/vue/src/views/CollectionEdit.vue
  33. 31 0
      src/test/java/com/izouma/nineth/repo/CollectionRepoTest.java
  34. 5 0
      src/test/java/com/izouma/nineth/repo/UserRepoTest.java
  35. 7 0
      src/test/java/com/izouma/nineth/service/AdapayMerchantServiceTest.java
  36. 7 0
      src/test/java/com/izouma/nineth/service/AssetServiceTest.java
  37. 2 2
      src/test/java/com/izouma/nineth/service/OrderServiceTest.java
  38. 66 3
      src/test/java/com/izouma/nineth/service/UserServiceTest.java

+ 5 - 0
src/main/java/com/izouma/nineth/config/EventNames.java

@@ -0,0 +1,5 @@
+package com.izouma.nineth.config;
+
+public class EventNames {
+    public final static String SWITCH_ACCOUNT = "switchAccount";
+}

+ 4 - 0
src/main/java/com/izouma/nineth/config/GeneralProperties.java

@@ -26,4 +26,8 @@ public class GeneralProperties {
     private String  updateActivityStockTopic;
     private int     dataCenterId;
     private int     workerId;
+    private String  updateQuotaGroup;
+    private String  updateQuotaTopic;
+    private String  broadcastEventGroup;
+    private String  broadcastEventTopic;
 }

+ 2 - 0
src/main/java/com/izouma/nineth/config/RedisKeys.java

@@ -9,6 +9,8 @@ public class RedisKeys {
 
     public static final String COLLECTION_SALE = "collectionSale::";
 
+    public static final String COLLECTION_QUOTA = "collectionQuota::";
+
     public static final String PAY_RECORD = "payRecord::";
 
     public static final String ORDER_LOCK = "orderLock::";

+ 11 - 0
src/main/java/com/izouma/nineth/domain/Collection.java

@@ -171,4 +171,15 @@ public class Collection extends BaseEntity {
     @ApiModelProperty("注册背景")
     private String registerBg;
 
+    @ApiModelProperty("总额度")
+    private Integer totalQuota;
+
+    @ApiModelProperty("剩余额度")
+    private Integer vipQuota;
+
+    @ApiModelProperty("延迟销售")
+    private Boolean timeDelay;
+
+    @ApiModelProperty("销售时间")
+    private LocalDateTime saleTime;
 }

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

@@ -37,6 +37,7 @@ import java.util.List;
         @Index(columnList = "collectionId"),
         @Index(columnList = "transactionId"),
         @Index(columnList = "minterId"),
+        @Index(columnList = "createdAt")
 })
 @AllArgsConstructor
 @NoArgsConstructor
@@ -205,4 +206,7 @@ public class Order extends BaseEntityNoID {
 
     @ApiModelProperty("是否vip")
     private boolean vip;
+
+    @ApiModelProperty("vip积分购买")
+    private Integer vipPoint;
 }

+ 29 - 0
src/main/java/com/izouma/nineth/domain/PointRecord.java

@@ -0,0 +1,29 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Index;
+import javax.persistence.Table;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Entity
+@Table(name = "point_record", indexes = {
+        @Index(columnList = "userId"),
+        @Index(columnList = "collectionId")
+})
+public class PointRecord extends BaseEntity {
+    private Long userId;
+
+    private Long collectionId;
+
+    private int point;
+
+    private String type;
+}

+ 7 - 1
src/main/java/com/izouma/nineth/domain/User.java

@@ -32,6 +32,7 @@ import java.util.Set;
         @Index(columnList = "collectionInvitor"),
         @Index(columnList = "admin"),
         @Index(columnList = "minter"),
+        @Index(columnList = "createdAt"),
         @Index(columnList = "settleAccountId")
 })
 @AllArgsConstructor
@@ -143,6 +144,11 @@ public class User extends BaseEntity implements Serializable {
 
     private boolean minter;
 
+    @Column(columnDefinition = "bit default false")
     @ApiModelProperty("使用藏品图片")
-    private boolean useCollectionPic;
+    private boolean useCollectionPic = false;
+
+    @Column(columnDefinition = "int(11) default 0")
+    @ApiModelProperty("白名单积分")
+    private int vipPoint = 0;
 }

+ 17 - 0
src/main/java/com/izouma/nineth/dto/adapay/DivMembersItem.java

@@ -0,0 +1,17 @@
+package com.izouma.nineth.dto.adapay;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+@Data
+public class DivMembersItem {
+
+    @JSONField(name = "member_id")
+    private String memberId;
+
+    @JSONField(name = "amount")
+    private String amount;
+
+    @JSONField(name = "fee_flag")
+    private String feeFlag;
+}

+ 88 - 0
src/main/java/com/izouma/nineth/dto/adapay/PaymentItem.java

@@ -0,0 +1,88 @@
+package com.izouma.nineth.dto.adapay;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class PaymentItem {
+
+    @JSONField(name = "order_no")
+    private String orderNo;
+
+    @JSONField(name = "created_time")
+    private String createdTime;
+
+    @JSONField(name = "pay_amt")
+    private String payAmt;
+
+    @JSONField(name = "open_id")
+    private String openId;
+
+    @JSONField(name = "confirmed_amt")
+    private String confirmedAmt;
+
+    @JSONField(name = "end_time")
+    private String endTime;
+
+    @JSONField(name = "fee_mode")
+    private String feeMode;
+
+    @JSONField(name = "coupon_infos")
+    private String couponInfos;
+
+    @JSONField(name = "discount_amt")
+    private String discountAmt;
+
+    @JSONField(name = "cash_pay_amt")
+    private String cashPayAmt;
+
+    @JSONField(name = "reserved_amt")
+    private String reservedAmt;
+
+    @JSONField(name = "out_trans_id")
+    private String outTransId;
+
+    @JSONField(name = "party_order_id")
+    private String partyOrderId;
+
+    @JSONField(name = "pay_mode")
+    private String payMode;
+
+    @JSONField(name = "div_members")
+    private List<DivMembersItem> divMembers;
+
+    @JSONField(name = "refunded_amt")
+    private String refundedAmt;
+
+    @JSONField(name = "prod_mode")
+    private String prodMode;
+
+    @JSONField(name = "pay_channel")
+    private String payChannel;
+
+    @JSONField(name = "has_more")
+    private boolean hasMore;
+
+    @JSONField(name = "id")
+    private String id;
+
+    @JSONField(name = "app_id")
+    private String appId;
+
+    @JSONField(name = "fee_amt")
+    private String feeAmt;
+
+    @JSONField(name = "object")
+    private String object;
+
+    @JSONField(name = "status")
+    private String status;
+
+    @JSONField(name = "error_msg")
+    private String errorMsg;
+
+    @JSONField(name = "error_code")
+    private String errorCode;
+}

+ 28 - 0
src/main/java/com/izouma/nineth/dto/adapay/PaymentList.java

@@ -0,0 +1,28 @@
+package com.izouma.nineth.dto.adapay;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class PaymentList {
+
+    @JSONField(name = "payments")
+    private List<PaymentItem> payments;
+
+    @JSONField(name = "prod_mode")
+    private String prodMode;
+
+    @JSONField(name = "has_more")
+    private boolean hasMore;
+
+    @JSONField(name = "app_id")
+    private String appId;
+
+    @JSONField(name = "object")
+    private String object;
+
+    @JSONField(name = "status")
+    private String status;
+}

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

@@ -26,5 +26,4 @@ public class CreateOrderEvent implements Serializable {
     @JsonSerialize(using = ToStringSerializer.class)
     private Long    invitor;
     private boolean vip;
-    private int     vipPurchase;
 }

+ 43 - 0
src/main/java/com/izouma/nineth/listener/BroadcastEventListener.java

@@ -0,0 +1,43 @@
+package com.izouma.nineth.listener;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.izouma.nineth.config.EventNames;
+import com.izouma.nineth.service.AdapayMerchantService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.ConsumeMode;
+import org.apache.rocketmq.spring.annotation.MessageModel;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+@RocketMQMessageListener(
+        messageModel = MessageModel.BROADCASTING,
+        consumerGroup = "${general.broadcast-event-group}",
+        topic = "${general.broadcast-event-topic}",
+        consumeMode = ConsumeMode.CONCURRENTLY)
+public class BroadcastEventListener implements RocketMQListener<JSONObject> {
+    private AdapayMerchantService adapayMerchantService;
+
+    @Override
+    public void onMessage(JSONObject message) {
+        log.info("接收到广播事件 {}", JSONObject.toJSONString(message, SerializerFeature.PrettyFormat));
+        String name = message.getString("name");
+        if (name != null) {
+            switch (name) {
+                case EventNames.SWITCH_ACCOUNT:
+                    try {
+                        Long id = message.getLong("data");
+                        adapayMerchantService.select(id);
+                    } catch (Exception e) {
+                        log.error("event error", e);
+                    }
+                    break;
+            }
+        }
+    }
+}

+ 1 - 1
src/main/java/com/izouma/nineth/listener/CreateOrderListener.java

@@ -37,7 +37,7 @@ public class CreateOrderListener implements RocketMQListener<CreateOrderEvent> {
         try {
             Order order = orderService.create(event.getUserId(), event.getCollectionId(), event.getQty(),
                     event.getAddressId(), event.getUserCouponId(), event.getInvitor(), event.getId(),
-                    event.isVip(), event.getVipPurchase());
+                    event.isVip());
             map.put("success", true);
             map.put("data", order);
         } catch (Exception e) {

+ 28 - 0
src/main/java/com/izouma/nineth/listener/UpdateQuotaListener.java

@@ -0,0 +1,28 @@
+package com.izouma.nineth.listener;
+
+import com.izouma.nineth.service.CollectionService;
+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.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+@RocketMQMessageListener(
+        consumerGroup = "${general.update-quota-group}",
+        topic = "${general.update-quota-topic}",
+        consumeMode = ConsumeMode.ORDERLY)
+@ConditionalOnProperty(value = "general.notify-server", havingValue = "false", matchIfMissing = true)
+public class UpdateQuotaListener implements RocketMQListener<Long> {
+
+    private CollectionService collectionService;
+
+    @Override
+    public void onMessage(Long id) {
+        collectionService.syncQuota(id);
+    }
+}

+ 10 - 0
src/main/java/com/izouma/nineth/repo/AssetRepo.java

@@ -58,4 +58,14 @@ public interface AssetRepo extends JpaRepository<Asset, Long>, JpaSpecificationE
     List<Asset> findByTxHashIsNullAndTokenIdNotNullAndCreatedAtBefore(LocalDateTime time);
 
     List<Asset> findAllByIdInAndUserId(Collection<Long> id, Long userId);
+
+    @Query(value = "select c.id, c.pic, c.model3d, c.minter_avatar, c.owner_avatar, c.detail from asset c", nativeQuery = true)
+    List<List<String>> selectResource();
+
+    @Modifying
+    @Transactional
+    @Query(value = "update asset c set c.pic = ?2, c.model3d = ?3, c.minter_avatar = ?4, " +
+            "c.owner_avatar = ?5, c.detail = ?6 where c.id = ?1", nativeQuery = true)
+    int updateCDN(Long id, String pic, String model3d, String minterAvatar,
+                  String ownerAvatar, String detail);
 }

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

@@ -30,12 +30,14 @@ public interface CollectionRepo extends JpaRepository<Collection, Long>, JpaSpec
             "c.schedule_sale = ?5, c.sort = ?6, c.detail = ?7, c.privileges = ?8, " +
             "c.properties = ?9, c.model3d = ?10, c.max_count = ?11, c.count_id = ?12, c.scan_code = ?13, " +
             "c.no_sold_out = ?14, c.assignment = ?15, c.coupon_payment = ?16, c.share_bg = ?17," +
-            "c.register_bg = ?18 where c.id = ?1", nativeQuery = true)
+            "c.register_bg = ?18, c.vip_quota = ?19, c.time_delay = ?20, c.sale_time = ?21 " +
+            "where c.id = ?1", nativeQuery = true)
     @CacheEvict(value = {"collection", "recommend"}, allEntries = true)
     void update(@Nonnull Long id, boolean onShelf, boolean salable, LocalDateTime startTime,
                 boolean schedule, int sort, String detail, String privileges,
                 String properties, String model3d, int maxCount, String countId, boolean scanCode,
-                boolean noSoldOut, int assignment, boolean couponPayment, String shareBg, String registerBg);
+                boolean noSoldOut, int assignment, boolean couponPayment, String shareBg, String registerBg,
+                Integer vipQuota, Boolean timeDelay, LocalDateTime saleTime);
 
     @Cacheable("collection")
     Optional<Collection> findById(@Nonnull Long id);
@@ -115,4 +117,26 @@ public interface CollectionRepo extends JpaRepository<Collection, Long>, JpaSpec
     List<CollectionStockAndSale> getStockAndSale();
 
     List<Collection> findAllByIdIn(java.util.Collection<Long> ids);
+
+    @Query(value = "select c.id, c.pic, c.model3d, c.minter_avatar, c.owner_avatar, c.detail from collection_info c", nativeQuery = true)
+    List<List<String>> selectResource();
+
+    @Query(value = "select c.id, c.pic, c.model3d, c.minter_avatar, c.owner_avatar, c.detail from collection_info c where c.id = ?1", nativeQuery = true)
+    List<List<String>> selectResource(Long id);
+
+    @Modifying
+    @Transactional
+    @Query(value = "update collection_info c set c.pic = ?2, c.model3d = ?3, c.minter_avatar = ?4, " +
+            "c.owner_avatar = ?5, c.detail = ?6 where c.id = ?1", nativeQuery = true)
+    int updateCDN(Long id, String pic, String model3d, String minterAvatar,
+                  String ownerAvatar, String detail);
+
+    @Query("update Collection c set c.vipQuota = ?2 where c.id = ?1")
+    @Transactional
+    @Modifying
+    int updateVipQuota(Long id, int vipQuota);
+
+    @Query("select c.vipQuota from Collection c where c.id = ?1")
+    Integer getVipQuota(Long id);
+
 }

+ 8 - 0
src/main/java/com/izouma/nineth/repo/PointRecordRepo.java

@@ -0,0 +1,8 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.PointRecord;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface PointRecordRepo extends JpaRepository<PointRecord, Long> {
+    int countByUserIdAndCollectionId(Long userId, Long collectionId);
+}

+ 7 - 1
src/main/java/com/izouma/nineth/repo/UserRepo.java

@@ -171,7 +171,7 @@ public interface UserRepo extends JpaRepository<User, Long>, JpaSpecificationExe
 
     List<User> findAllByCollectionIdAndCollectionInvitor(Long collectionId, Long collectionInvitor);
 
-    long countAllByCollectionIdAndCollectionInvitor(Long collectionId, Long collectionInvitor);
+    int countAllByCollectionIdAndCollectionInvitor(Long collectionId, Long collectionInvitor);
 
     long countAllByAuthoritiesContainsAndDelFalse(Authority authority);
 
@@ -180,4 +180,10 @@ public interface UserRepo extends JpaRepository<User, Long>, JpaSpecificationExe
     List<User> findAllByCreatedAtIsAfterAndAuthoritiesContains(LocalDateTime createdAt, Authority authorities);
 
     List<User> findBySettleAccountIdIsNotNull();
+
+    @Transactional
+    @Modifying
+    @Query("update User set vipPoint = vipPoint + ?2 where id = ?1")
+    void updateVipPoint(Long id, int num);
+
 }

+ 44 - 13
src/main/java/com/izouma/nineth/service/AdapayMerchantService.java

@@ -1,14 +1,19 @@
 package com.izouma.nineth.service;
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.huifu.adapay.Adapay;
 import com.huifu.adapay.core.exception.BaseAdaPayException;
 import com.huifu.adapay.model.*;
 import com.izouma.nineth.config.AdapayProperties;
+import com.izouma.nineth.config.EventNames;
+import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.domain.AdapayMerchant;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.dto.adapay.MemberInfo;
+import com.izouma.nineth.dto.adapay.PaymentItem;
+import com.izouma.nineth.dto.adapay.PaymentList;
 import com.izouma.nineth.dto.adapay.SettleAccountsItem;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.AdapayMerchantRepo;
@@ -18,6 +23,7 @@ import com.izouma.nineth.utils.SnowflakeIdWorker;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.MapUtils;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.data.domain.Page;
 import org.springframework.stereotype.Service;
 
@@ -34,6 +40,8 @@ public class AdapayMerchantService {
 
     private final AdapayMerchantRepo adapayMerchantRepo;
     private final AdapayProperties   adapayProperties;
+    private final RocketMQTemplate   rocketMQTemplate;
+    private final GeneralProperties  generalProperties;
 
     @PostConstruct
     public void init() {
@@ -72,6 +80,19 @@ public class AdapayMerchantService {
         return record;
     }
 
+    public void sendSelectEvent(Long id) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("name", EventNames.SWITCH_ACCOUNT);
+        jsonObject.put("data", id);
+        rocketMQTemplate.convertAndSend(generalProperties.getBroadcastEventTopic(), jsonObject);
+
+        try {
+            Thread.sleep(500);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
     public void select(Long id) throws Exception {
         AdapayMerchant merchant = adapayMerchantRepo.findById(id).orElseThrow(new BusinessException("商户不存在"));
 
@@ -229,24 +250,34 @@ public class AdapayMerchantService {
 
     public Object query(Long merchantId, String id) throws BaseAdaPayException {
         AdapayMerchant merchant = adapayMerchantRepo.findById(merchantId).orElseThrow(new BusinessException("商户不存在"));
-        Map<String, Object> map = Payment.query(id, merchant.getName());
-        log.info(JSON.toJSONString(map, SerializerFeature.PrettyFormat));
-        return map;
+        Map<String, Object> paymentParams = new HashMap<>();
+        paymentParams.put("app_id", merchant.getAppId());
+        paymentParams.put("payment_id", id);
+        Map<String, Object> paymentList = Payment.queryList(paymentParams, merchant.getName());
+        log.info(JSON.toJSONString(paymentList, SerializerFeature.PrettyFormat));
+        return paymentList;
     }
 
     public Object refund(Long merchantId, String id) throws BaseAdaPayException {
         AdapayMerchant merchant = adapayMerchantRepo.findById(merchantId).orElseThrow(new BusinessException("商户不存在"));
-        Map<String, Object> map = Payment.query(id, merchant.getName());
-        if (!"succeeded".equals(MapUtils.getString(map, "status"))) {
-            return map;
+
+        Map<String, Object> paymentParams = new HashMap<>();
+        paymentParams.put("app_id", merchant.getAppId());
+        paymentParams.put("payment_id", id);
+        Map<String, Object> res = Payment.queryList(paymentParams, merchant.getName());
+        log.info(JSON.toJSONString(res, SerializerFeature.PrettyFormat));
+        PaymentList paymentList = JSON.parseObject(JSON.toJSONString(res), PaymentList.class);
+
+        if (paymentList.getPayments() != null && paymentList.getPayments().size() == 1) {
+            PaymentItem paymentItem = paymentList.getPayments().get(0);
+            Map<String, Object> refundParams = new HashMap<>();
+            refundParams.put("refund_amt", paymentItem.getPayAmt());
+            refundParams.put("refund_order_no", new SnowflakeIdWorker(0, 0).nextId() + "");
+            Map<String, Object> response = Refund.create(id, refundParams, merchant.getName());
+            log.info(JSON.toJSONString(response, SerializerFeature.PrettyFormat));
+            return response;
         }
-        String amt = MapUtils.getString(map, "pay_amt");
-        Map<String, Object> refundParams = new HashMap<>();
-        refundParams.put("refund_amt", amt);
-        refundParams.put("refund_order_no", new SnowflakeIdWorker(0, 0).nextId() + "");
-        Map<String, Object> response = Refund.create(id, refundParams, merchant.getName());
-        log.info(JSON.toJSONString(response, SerializerFeature.PrettyFormat));
-        return response;
+        return res;
     }
 
     public static void checkSuccess(Map<String, Object> map) {

+ 12 - 1
src/main/java/com/izouma/nineth/service/AssetMintService.java

@@ -17,11 +17,13 @@ import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.context.ApplicationContext;
+import org.springframework.core.env.Environment;
 import org.springframework.retry.annotation.Backoff;
 import org.springframework.retry.annotation.Retryable;
 import org.springframework.stereotype.Service;
 
 import java.io.File;
+import java.util.Arrays;
 
 @Service
 @Slf4j
@@ -31,6 +33,7 @@ public class AssetMintService {
     private UserRepo           userRepo;
     private NFTService         nftService;
     private ApplicationContext applicationContext;
+    private Environment        env;
 
     @Retryable(maxAttempts = 10, backoff = @Backoff(delay = 1000))
     public void mint(Long assetId) {
@@ -93,7 +96,15 @@ public class AssetMintService {
 
     public String ipfsUpload(String url) {
         try {
-            IPFS ipfs = new IPFS("120.24.204.226", 5001);
+            url = url.replace("raex-meta.oss-cn-shenzhen.aliyuncs.com",
+                            "raex-meta.oss-cn-shenzhen-internal.aliyuncs.com")
+                    .replace("cdn.raex.vip",
+                            "raex-meta.oss-cn-shenzhen-internal.aliyuncs.com");
+            String host = "120.24.204.226";
+            if (Arrays.asList(env.getActiveProfiles()).contains("prod")) {
+                host = "172.29.50.102";
+            }
+            IPFS ipfs = new IPFS(host, 5001);
             HttpRequest request = HttpRequest.get(url);
             File file = File.createTempFile("ipfs", ".tmp");
             request.receive(file);

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

@@ -1,7 +1,6 @@
 package com.izouma.nineth.service;
 
 import cn.hutool.core.convert.Convert;
-import cn.hutool.core.util.ObjectUtil;
 import com.izouma.nineth.TokenHistory;
 import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.domain.Collection;
@@ -35,6 +34,8 @@ import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
 import java.util.stream.Collectors;
 
 @Service
@@ -455,4 +456,39 @@ public class AssetService {
         map.put("buy", buy);
         return map;
     }
+
+    public void transferCDN() throws ExecutionException, InterruptedException {
+        ForkJoinPool customThreadPool = new ForkJoinPool(100);
+        customThreadPool.submit(() -> {
+            collectionRepo.selectResource().parallelStream().forEach(list -> {
+                for (int i = 0; i < list.size(); i++) {
+                    list.set(i, replaceCDN(list.get(i)));
+                }
+                collectionRepo.updateCDN(Long.parseLong(list.get(0)),
+                        list.get(1),
+                        list.get(2),
+                        list.get(3),
+                        list.get(4),
+                        list.get(5));
+            });
+
+            assetRepo.selectResource().parallelStream().forEach(list -> {
+                for (int i = 0; i < list.size(); i++) {
+                    list.set(i, replaceCDN(list.get(i)));
+                }
+                assetRepo.updateCDN(Long.parseLong(list.get(0)),
+                        list.get(1),
+                        list.get(2),
+                        list.get(3),
+                        list.get(4),
+                        list.get(5));
+            });
+        }).get();
+    }
+
+    public String replaceCDN(String url) {
+        if (url == null) return null;
+        return url.replaceAll("https://raex-meta\\.oss-cn-shenzhen\\.aliyuncs\\.com",
+                "https://cdn.raex.vip");
+    }
 }

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

@@ -151,7 +151,8 @@ public class CollectionService {
                 record.getDetail(), JSON.toJSONString(record.getPrivileges()),
                 JSON.toJSONString(record.getProperties()), JSON.toJSONString(record.getModel3d()),
                 record.getMaxCount(), record.getCountId(), record.isScanCode(), record.isNoSoldOut(),
-                record.getAssignment(), record.isCouponPayment(), record.getShareBg(), record.getRegisterBg());
+                record.getAssignment(), record.isCouponPayment(), record.getShareBg(), record.getRegisterBg(),
+                record.getVipQuota(), record.getTimeDelay(), record.getSaleTime());
 
         record = collectionRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
         onShelfTask(record);
@@ -195,7 +196,7 @@ public class CollectionService {
                 if (collection.getType() == CollectionType.BLIND_BOX) {
                     collectionDTO.setAppointment(appointmentRepo.findFirstByBlindBoxId(collection.getId()).isPresent());
                 }
-                if (showVip && user.getVipPurchase() > 0) {
+                if (showVip && collection.getAssignment() > 0 && user.getVipPurchase() > 0) {
                     int purchase = orderRepo.countByUserIdAndCollectionIdAndVipTrueAndStatusIn(user.getId(), collection.getId(), Arrays.asList(OrderStatus.FINISH, OrderStatus.NOT_PAID, OrderStatus.PROCESSING));
                     collectionDTO.setVipSurplus(user.getVipPurchase() - purchase);
                 }
@@ -416,4 +417,26 @@ public class CollectionService {
             cacheService.clearCollection(id);
         }
     }
+
+    @Debounce(key = "#id", delay = 500)
+    public void syncQuota(Long id) {
+        Integer quota = (Integer) redisTemplate.opsForValue().get(RedisKeys.COLLECTION_QUOTA + id);
+        if (quota != null) {
+            log.info("同步额度信息{}", id);
+            collectionRepo.updateVipQuota(id, quota);
+            cacheService.clearCollection(id);
+        }
+    }
+
+    public synchronized Long decreaseQuota(Long id, int number) {
+        BoundValueOperations<String, Object> ops = redisTemplate.boundValueOps(RedisKeys.COLLECTION_QUOTA + id);
+        if (ops.get() == null) {
+            Boolean success = ops.setIfAbsent(Optional.ofNullable(collectionRepo.getVipQuota(id))
+                    .orElse(0), 7, TimeUnit.DAYS);
+            log.info("创建redis额度:{}", success);
+        }
+        Long stock = ops.increment(-number);
+        rocketMQTemplate.convertAndSend(generalProperties.getUpdateQuotaTopic(), id);
+        return stock;
+    }
 }

+ 32 - 8
src/main/java/com/izouma/nineth/service/OrderService.java

@@ -97,7 +97,7 @@ public class OrderService {
     }
 
     public String mqCreate(Long userId, Long collectionId, int qty, Long addressId, Long userCouponId, Long invitor,
-                           String sign, boolean vip, int vipPurchase) {
+                           String sign, boolean vip, int vipPurchase, int vipPoint) {
         String qs = null;
         try {
             qs = AESEncryptUtil.decrypt(sign);
@@ -111,13 +111,14 @@ public class OrderService {
 
         Long id = snowflakeIdWorker.nextId();
         SendResult result = rocketMQTemplate.syncSend(generalProperties.getCreateOrderTopic(),
-                new CreateOrderEvent(id, userId, collectionId, qty, addressId, userCouponId, invitor, vip, vipPurchase), 100000);
+                new CreateOrderEvent(id, userId, collectionId, qty, addressId, userCouponId, invitor, vip), 100000);
+
         log.info("发送订单到队列: {}, userId={}, result={}", id, userId, result);
         return String.valueOf(id);
     }
 
     public Order create(Long userId, Long collectionId, int qty, Long addressId, Long userCouponId, Long invitor,
-                        Long id, boolean vip, int vipPurchase) {
+                        Long id, boolean vip) {
         long t = System.currentTimeMillis();
         qty = 1;
         int stock = Optional.ofNullable(collectionService.decreaseStock(collectionId, qty))
@@ -181,18 +182,30 @@ public class OrderService {
             }
 
             //查询是否有拉新任务,只算官方购买
+            int usePoint = 0;
             if (collection.getSource() != CollectionSource.TRANSFER && collection.getAssignment() > 0) {
+                //延迟销售
+                if (collection.getTimeDelay()) {
+                    if (collection.getSaleTime().isAfter(LocalDateTime.now())) {
+                        throw new BusinessException("当前还未开售");
+                    }
+                }
+                User user = userRepo.findById(userId).orElseThrow(new BusinessException("用户不存在"));
                 if (vip) {
                     int purchase = orderRepo.countByUserIdAndCollectionIdAndVipTrueAndStatusIn(userId, collectionId, Arrays.asList(OrderStatus.FINISH, OrderStatus.NOT_PAID, OrderStatus.PROCESSING));
-                    if (vipPurchase - purchase <= 0) {
+                    if (user.getVipPurchase() - purchase <= 0) {
                         throw new BusinessException("vip名额已使用完毕!");
                     }
                 } else {
-                    long count = userRepo.countAllByCollectionIdAndCollectionInvitor(collectionId, userId);
-                    int sub = collection.getAssignment() - (int) count;
-                    if (sub > 0) {
-                        throw new BusinessException("再拉新" + sub + "人即可购买");
+//                    long count = userRepo.countAllByCollectionIdAndCollectionInvitor(collectionId, userId);
+//                    int sub = collection.getAssignment() - (int) count;
+//                    if (sub > 0) {
+//                        throw new BusinessException("再拉新" + sub + "人即可购买");
+//                    }
+                    if (user.getVipPoint() < 1) {
+                        throw new BusinessException("没有购买名额");
                     }
+                    usePoint = 1;
                 }
             }
 
@@ -234,6 +247,7 @@ public class OrderService {
                     .invitor(invitor)
                     .countId(collection.getCountId())
                     .vip(vip)
+                    .vipPoint(usePoint)
                     .build();
             if (coupon != null) {
                 coupon.setUsed(true);
@@ -256,6 +270,11 @@ public class OrderService {
             if (order.getTotalPrice().equals(BigDecimal.ZERO)) {
                 notifyOrder(order.getId(), PayMethod.WEIXIN, null);
             }
+
+            if (usePoint > 0) {
+                // 扣除积分
+                userRepo.updateVipPoint(userId, -usePoint);
+            }
             rocketMQTemplate.syncSend(generalProperties.getUpdateStockTopic(), collectionId, 10000);
             log.info("订单创建完成, id={}, {}ms", order.getId(), System.currentTimeMillis() - t);
             return order;
@@ -704,6 +723,11 @@ public class OrderService {
                     userCouponRepo.save(coupon);
                 });
             }
+            //加上积分
+            if (order.getVipPoint() > 0) {
+                userRepo.updateVipPoint(order.getUserId(), order.getVipPoint());
+            }
+
             rocketMQTemplate.syncSend(generalProperties.getUpdateStockTopic(), order.getCollectionId(), 10000);
             log.info("取消订单{}", order.getId());
         } catch (Exception e) {

+ 32 - 8
src/main/java/com/izouma/nineth/service/UserService.java

@@ -5,7 +5,6 @@ import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
 import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
 import com.huifu.adapay.core.exception.BaseAdaPayException;
 import com.izouma.nineth.TokenHistory;
-import com.izouma.nineth.config.AdapayProperties;
 import com.izouma.nineth.config.Constants;
 import com.izouma.nineth.domain.Collection;
 import com.izouma.nineth.domain.*;
@@ -34,7 +33,6 @@ import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.context.ApplicationContext;
 import org.springframework.context.event.EventListener;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
@@ -62,21 +60,19 @@ public class UserService {
     private SmsService            smsService;
     private StorageService        storageService;
     private JwtTokenUtil          jwtTokenUtil;
-    private CaptchaService        captchaService;
     private FollowService         followService;
     private FollowRepo            followRepo;
     private IdentityAuthRepo      identityAuthRepo;
     private SysConfigService      sysConfigService;
-    private AdapayService         adapayService;
     private UserBankCardRepo      userBankCardRepo;
     private InviteRepo            inviteRepo;
     private NFTService            nftService;
     private CacheService          cacheService;
-    private ApplicationContext    context;
     private TokenHistoryRepo      tokenHistoryRepo;
     private CollectionRepo        collectionRepo;
-    private AdapayProperties      adapayProperties;
     private AdapayMerchantService adapayMerchantService;
+    private PointRecordRepo       pointRecordRepo;
+    private CollectionService     collectionService;
 
     public User update(User user) {
         User orig = userRepo.findById(user.getId()).orElseThrow(new BusinessException("无记录"));
@@ -165,8 +161,9 @@ public class UserService {
             invite = inviteRepo.findFirstByCode(inviteCode).orElse(null);
         }
         smsService.verify(phone, code);
+        Collection collection = null;
         if (collectionId != null) {
-            Collection collection = collectionRepo.findById(collectionId).orElseThrow(new BusinessException("无藏品"));
+            collection = collectionRepo.findById(collectionId).orElseThrow(new BusinessException("无藏品"));
             if (!collection.isOnShelf() || !collection.isSalable()) {
                 collectionId = null;
             } else if (collection.isScheduleSale()) {
@@ -191,6 +188,33 @@ public class UserService {
         if (invite != null) {
             inviteRepo.increaseNum(invite.getId());
         }
+
+        // 加积分
+        if (collectionId != null && invitor != null) {
+            // 额度
+            if (collection.getVipQuota() > 0) {
+                int countUser = userRepo.countAllByCollectionIdAndCollectionInvitor(collectionId, invitor);
+                // 邀请人数
+                if (countUser >= collection.getAssignment()) {
+                    int point = pointRecordRepo.countByUserIdAndCollectionId(invitor, collectionId);
+                    // 是否已有积分
+                    if (point <= 0) {
+                        long count = userRepo.countAllByCollectionIdAndCollectionInvitor(collectionId, invitor);
+                        if (count >= collection.getAssignment()) {
+                            userRepo.updateVipPoint(invitor, 1);
+                            pointRecordRepo.save(PointRecord.builder()
+                                    .collectionId(collectionId)
+                                    .userId(invitor)
+                                    .type("VIP_POINT")
+                                    .point(1)
+                                    .build());
+                            // 扣除藏品额度
+                            collectionService.decreaseQuota(collectionId, 1);
+                        }
+                    }
+                }
+            }
+        }
         return user;
     }
 
@@ -495,7 +519,7 @@ public class UserService {
         String accountId = adapayMerchantService.createSettleAccountForAll
                 (user.getMemberId(), identityAuth.getRealName(),
                         identityAuth.getIdNo(), phone, bankNo);
-        user.setSettleAccountId(accountId);
+        user.setSettleAccountId(Optional.ofNullable(accountId).orElse("1"));
         userRepo.save(user);
 
         userBankCardRepo.save(UserBankCard.builder()

+ 1 - 1
src/main/java/com/izouma/nineth/web/AdapayMerchantController.java

@@ -56,7 +56,7 @@ public class AdapayMerchantController extends BaseController {
 
     @PostMapping("/select")
     public void select(@RequestParam Long id) throws Exception {
-        adapayMerchantService.select(id);
+        adapayMerchantService.sendSelectEvent(id);
     }
 
     @PostMapping("/query")

+ 15 - 0
src/main/java/com/izouma/nineth/web/AssetController.java

@@ -16,6 +16,7 @@ import com.izouma.nineth.utils.excel.ExcelUtils;
 import io.swagger.annotations.ApiOperation;
 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;
@@ -23,6 +24,7 @@ import java.io.IOException;
 import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ExecutionException;
 
 @RestController
 @RequestMapping("/asset")
@@ -128,6 +130,19 @@ public class AssetController extends BaseController {
     public Map<String, BigDecimal> breakdown() {
         return assetService.breakdown(SecurityUtils.getAuthenticatedUser().getId());
     }
+
+    @PostMapping("/cdn")
+    @PreAuthorize("hasRole('ADMIN')")
+    public String cdn() throws ExecutionException, InterruptedException {
+        new Thread(() -> {
+            try {
+                assetService.transferCDN();
+            } catch (ExecutionException | InterruptedException e) {
+                e.printStackTrace();
+            }
+        }).start();
+        return "ok";
+    }
 }
 
 

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

@@ -112,7 +112,7 @@ public class OrderController extends BaseController {
         final User user = SecurityUtils.getAuthenticatedUser();
         return new HashMap<>() {{
             put("id", orderService.mqCreate(user.getId(), collectionId, qty, addressId, couponId, invitor, sign,
-                    vip, user.getVipPurchase()));
+                    vip, user.getVipPurchase(), user.getVipPoint()));
         }};
     }
 

+ 22 - 0
src/main/resources/application.yaml

@@ -153,6 +153,10 @@ general:
   mint-topic: mint-topic-dev
   update-activity-stock-group: update-activity-stock-group-dev
   update-activity-stock-topic: update-activity-stock-topic-dev
+  update-quota-group: update-quota-group-dev
+  update-quota-topic: update-quota-topic-dev
+  broadcast-event-group: broadcast-event-group-dev
+  broadcast-event-topic: broadcast-event-topic-dev
 mychain:
   rest:
     bizid: a00e36c5
@@ -229,6 +233,10 @@ general:
   mint-topic: mint-topic-test
   update-activity-stock-group: update-activity-stock-group-test
   update-activity-stock-topic: update-activity-stock-topic-test
+  update-quota-group: update-quota-group-test
+  update-quota-topic: update-quota-topic-test
+  broadcast-event-group: broadcast-event-group-test
+  broadcast-event-topic: broadcast-event-topic-test
 ---
 
 spring:
@@ -256,6 +264,10 @@ general:
   notify-server: true
   update-activity-stock-group: update-activity-stock-group-test1
   update-activity-stock-topic: update-activity-stock-topic-test1
+  update-quota-group: update-quota-group-test1
+  update-quota-topic: update-quota-topic-test1
+  broadcast-event-group: broadcast-event-group-test1
+  broadcast-event-topic: broadcast-event-topic-test1
 wx:
   pay:
     notify-url: https://test1.raex.vip/notify/order/weixin
@@ -308,6 +320,10 @@ general:
   mint-topic: mint-topic
   update-activity-stock-group: update-activity-stock-group
   update-activity-stock-topic: update-activity-stock-topic
+  update-quota-group: update-quota-group
+  update-quota-topic: update-quota-topic
+  broadcast-event-group: broadcast-event-group
+  broadcast-event-topic: broadcast-event-topic
 wx:
   pay:
     notify-url: https://www.raex.vip/notify/order/weixin
@@ -352,6 +368,8 @@ general:
   notify-server: true
   update-activity-stock-group: update-activity-stock-group
   update-activity-stock-topic: update-activity-stock-topic
+  update-quota-group: update-quota-group
+  update-quota-topic: update-quota-topic
 wx:
   pay:
     notify-url: https://www.raex.vip/notify/order/weixin
@@ -394,6 +412,10 @@ general:
   notify-server: true
   update-activity-stock-group: update-activity-stock-group-test
   update-activity-stock-topic: update-activity-stock-topic-test
+  update-quota-group: update-quota-group-test
+  update-quota-topic: update-quota-topic-test
+  broadcast-event-group: broadcast-event-group-test
+  broadcast-event-topic: broadcast-event-topic-test
 rocketmq:
   name-server: 172.29.50.102:9876
   producer:

BIN=BIN
src/main/vue/public/favicon.ico


+ 2 - 2
src/main/vue/public/index.html

@@ -4,9 +4,9 @@
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
-    <link rel="icon" href="<%= BASE_URL %>favicon.png">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <script src="<%= BASE_URL %>fontawesome-v5.13.0.js"></script>
-    <title>管理后台</title>
+    <title>绿洲管理后台</title>
   </head>
   <body>
     <noscript>

+ 109 - 54
src/main/vue/src/views/BlindBoxEdit.vue

@@ -193,28 +193,60 @@
                         <el-radio v-model="formData.noSoldOut" :label="false">是</el-radio>
                         <el-radio v-model="formData.noSoldOut" :label="true">否</el-radio>
                     </el-form-item>
-                    <el-form-item prop="assignment" label="拉新任务指标">
-                        <el-input-number
-                            type="number"
-                            :min="0"
-                            :step="1"
-                            :max="5"
-                            v-model="formData.assignment"
-                        ></el-input-number>
-                        <div class="tip">0表示无拉新任务限制</div>
+                    <el-form-item prop="couponPayment" label="支付方式">
+                        <el-radio-group v-model="formData.couponPayment">
+                            <el-radio :label="true">兑换券</el-radio>
+                            <el-radio :label="false">支付宝/微信</el-radio>
+                        </el-radio-group>
                     </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="assignment" label="拉新任务指标">
+                            <el-input-number
+                                type="number"
+                                :min="0"
+                                :step="1"
+                                :max="5"
+                                v-model="formData.assignment"
+                            ></el-input-number>
+                            <div class="tip">0表示无拉新任务限制</div>
+                        </el-form-item>
+                        <el-form-item prop="totalQuota" label="白名单额度" v-if="formData.assignment > 0">
+                            <el-input-number
+                                type="number"
+                                :min="0"
+                                :step="1"
+                                v-model="formData.totalQuota"
+                                :disabled="!editQuota"
+                            ></el-input-number>
+                            <div class="tip">多少人拉新可获得积分</div>
+                        </el-form-item>
+                    </div>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="timeDelay" label="延迟销售" v-if="formData.assignment > 0">
+                            <el-radio v-model="formData.timeDelay" :label="true">是</el-radio>
+                            <el-radio v-model="formData.timeDelay" :label="false">否</el-radio>
+                        </el-form-item>
+                        <el-form-item
+                            prop="saleTime"
+                            label="销售时间"
+                            v-if="formData.assignment > 0 && formData.timeDelay"
+                            style="margin-left: 22px"
+                        >
+                            <el-date-picker
+                                v-model="formData.saleTime"
+                                type="datetime"
+                                value-format="yyyy-MM-dd HH:mm:ss"
+                                placeholder="销售时间"
+                            ></el-date-picker>
+                        </el-form-item>
+                    </div>
+
                     <el-form-item label="分享海报" v-if="formData.assignment > 0">
                         <single-upload v-model="formData.shareBg"></single-upload>
                     </el-form-item>
                     <el-form-item label="注册海报" v-if="formData.assignment > 0">
                         <single-upload v-model="formData.registerBg"></single-upload>
                     </el-form-item>
-                    <el-form-item prop="couponPayment" label="支付方式">
-                        <el-radio-group v-model="formData.couponPayment">
-                            <el-radio :label="true">兑换券</el-radio>
-                            <el-radio :label="false">支付宝/微信</el-radio>
-                        </el-radio-group>
-                    </el-form-item>
                     <el-form-item class="form-submit">
                         <el-button @click="onSave" :loading="saving" type="primary" v-if="!formData.id">
                             保存
@@ -331,6 +363,9 @@ export default {
                             res.properties = res.properties || [];
                             res.privileges = res.privileges || [];
                             this.formData = res;
+                            if (res.totalQuota !== res.vipQuota) {
+                                this.editQuota = false;
+                            }
                             resolve();
                         })
                         .catch(e => {
@@ -375,43 +410,6 @@ export default {
                 }
             });
         });
-        // this.formData = {
-        //     name: 'OASISPUNK绿洲朋克',
-        //     pic: ['https://awesomeadmin.oss-cn-hangzhou.aliyuncs.com/image/2021-10-21-16-44-52kZqxuwhH.gif'],
-        //     minter: '管理员',
-        //     minterId: 1,
-        //     minterAvatar: 'https://awesomeadmin.oss-cn-hangzhou.aliyuncs.com/image/avatar_male.png',
-        //     detail:
-        //         '<div class="content-item" data-v-38285332="">\n<div data-v-38285332="">RAEX绿洲数字藏品中心首次携手星火爱心公益基金及火链Labs,联合发行公益型数字藏品: OASISPUNK绿洲朋克。OASISPUNK绿洲朋克是完全使用算法合成的加密人物头像,仅铸造发行3100枚,每一枚全都不同,更有稀缺度之分。绿洲朋克共分为3种类型:初代目(1500枚),次代目(1500枚),旗帜版(100枚)。每售出一枚绿洲朋克,收益所得将捐赠一定比例给&ldquo;星火爱心公益基金&rdquo;,用于扶贫助困类公益项目,同时买家将收到由星火爱心公益基金颁发的捐款证明,获得投身公益事业的荣誉感。欢迎你来到绿洲元宇宙,共建绿洲生态,共享绿洲文明荣耀。</div>\n</div>',
-        //     type: 'BLIND_BOX',
-        //     source: 'OFFICIAL',
-        //     sale: 0,
-        //     stock: 0,
-        //     total: 23,
-        //     likes: 0,
-        //     onShelf: true,
-        //     salable: true,
-        //     price: 0.01,
-        //     properties: [],
-        //     canResale: false,
-        //     royalties: 0,
-        //     serviceCharge: 0,
-        // };
-        // this.blindBoxItems = [
-        //     {
-        //         collectionId: 212,
-        //         total: 3,
-        //         rare: true
-        //     },
-        //     {
-        //         collectionId: 207,
-        //         total: 6
-        //     },
-        //     {
-        //         collectionId: 201,
-        //         total: 14
-        //     }
-        // ];
     },
     computed: {
         canEdit() {
@@ -586,7 +584,60 @@ export default {
                         }
                     }
                 ],
-                category: [{ required: true, message: '请填写分类' }]
+                category: [{ required: true, message: '请填写分类' }],
+                saleTime: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.timeDelay) {
+                                if (!value) {
+                                    callback(new Error('请填写销售时间'));
+                                } else if (isBefore(parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()), new Date())) {
+                                    callback(new Error('销售时间不能小于当前时间'));
+                                } else if (this.formData.scheduleSale) {
+                                    if (
+                                        isBefore(
+                                            parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()),
+                                            parse(this.formData.startTime, 'yyyy-MM-dd HH:mm:ss', new Date())
+                                        )
+                                    ) {
+                                        callback(new Error('销售时间不能小于发布时间'));
+                                    }
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                timeDelay: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.assignment > 0) {
+                                if (value === '' || value === undefined) {
+                                    callback(new Error('请选择是否延迟销售'));
+                                    return;
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                totalQuota: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.assignment > 0) {
+                                if (value === '' || value === undefined) {
+                                    callback(new Error('请输入白名单额度'));
+                                    return;
+                                }
+                            }
+
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ]
             },
             typeOptions: [
                 { label: '默认', value: 'DEFAULT' },
@@ -614,7 +665,8 @@ export default {
             privelegeRules: {
                 detail: [{ required: true, message: '请填写内容' }],
                 remark: [{ required: true, message: '请填写说明' }]
-            }
+            },
+            editQuota: true
         };
     },
     methods: {
@@ -628,6 +680,9 @@ export default {
             });
         },
         submit() {
+              if (this.editQuota && this.formData.totalQuota) {
+                this.formData.vipQuota = this.formData.totalQuota;
+            }
             if (this.formData.id) {
                 this.saving = true;
                 this.$http

+ 109 - 11
src/main/vue/src/views/CollectionEdit.vue

@@ -179,6 +179,7 @@
                         <el-radio v-model="formData.onShelf" :label="true">是</el-radio>
                         <el-radio v-model="formData.onShelf" :label="false">否</el-radio>
                     </el-form-item>
+
                     <div class="inline-wrapper">
                         <el-form-item prop="startTime" label="定时发布">
                             <el-radio v-model="formData.scheduleSale" :label="true">是</el-radio>
@@ -198,6 +199,7 @@
                             ></el-date-picker>
                         </el-form-item>
                     </div>
+
                     <el-form-item
                         prop="salable"
                         label="可售"
@@ -234,22 +236,55 @@
                             <el-radio :label="false">支付宝/微信</el-radio>
                         </el-radio-group>
                     </el-form-item>
-                    <el-form-item prop="assignment" label="拉新任务指标">
-                        <el-input-number
-                            type="number"
-                            :min="0"
-                            :step="1"
-                            :max="5"
-                            v-model="formData.assignment"
-                        ></el-input-number>
-                        <div class="tip">0表示无拉新任务限制</div>
-                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="assignment" label="拉新任务指标">
+                            <el-input-number
+                                type="number"
+                                :min="0"
+                                :step="1"
+                                :max="5"
+                                v-model="formData.assignment"
+                            ></el-input-number>
+                            <div class="tip">0表示无拉新任务限制</div>
+                        </el-form-item>
+                        <el-form-item prop="totalQuota" label="白名单额度" v-if="formData.assignment > 0">
+                            <el-input-number
+                                type="number"
+                                :min="0"
+                                :step="1"
+                                v-model="formData.totalQuota"
+                                :disabled="!editQuota"
+                            ></el-input-number>
+                            <div class="tip">多少人拉新可获得积分</div>
+                        </el-form-item>
+                    </div>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="timeDelay" label="延迟销售" v-if="formData.assignment > 0">
+                            <el-radio v-model="formData.timeDelay" :label="true">是</el-radio>
+                            <el-radio v-model="formData.timeDelay" :label="false">否</el-radio>
+                        </el-form-item>
+                        <el-form-item
+                            prop="saleTime"
+                            label="销售时间"
+                            v-if="formData.assignment > 0 && formData.timeDelay"
+                            style="margin-left: 22px"
+                        >
+                            <el-date-picker
+                                v-model="formData.saleTime"
+                                type="datetime"
+                                value-format="yyyy-MM-dd HH:mm:ss"
+                                placeholder="销售时间"
+                            ></el-date-picker>
+                        </el-form-item>
+                    </div>
+
                     <el-form-item label="分享海报" v-if="formData.assignment > 0">
                         <single-upload v-model="formData.shareBg"></single-upload>
                     </el-form-item>
                     <el-form-item label="注册海报" v-if="formData.assignment > 0">
                         <single-upload v-model="formData.registerBg"></single-upload>
                     </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">
@@ -318,6 +353,9 @@ export default {
                             res.properties = res.properties || [];
                             res.privileges = res.privileges || [];
                             this.formData = res;
+                            if (res.totalQuota !== res.vipQuota) {
+                                this.editQuota = false;
+                            }
                             resolve();
                         })
                         .catch(e => {
@@ -509,6 +547,59 @@ export default {
                         },
                         trigger: 'blur'
                     }
+                ],
+                saleTime: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.timeDelay) {
+                                if (!value) {
+                                    callback(new Error('请填写销售时间'));
+                                } else if (isBefore(parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()), new Date())) {
+                                    callback(new Error('销售时间不能小于当前时间'));
+                                } else if (this.formData.scheduleSale) {
+                                    if (
+                                        isBefore(
+                                            parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()),
+                                            parse(this.formData.startTime, 'yyyy-MM-dd HH:mm:ss', new Date())
+                                        )
+                                    ) {
+                                        callback(new Error('销售时间不能小于发布时间'));
+                                    }
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                timeDelay: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.assignment > 0) {
+                                if (value === '' || value === undefined) {
+                                    callback(new Error('请选择是否延迟销售'));
+                                    return;
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                totalQuota: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.assignment > 0) {
+                                if (value === '' || value === undefined) {
+                                    callback(new Error('请输入白名单额度'));
+                                    return;
+                                }
+                            }
+
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
                 ]
             },
             typeOptions: [
@@ -531,7 +622,8 @@ export default {
             },
             customUrl: resolveUrl(this.$baseUrl, 'upload/3dModel'),
             scale: 1,
-            yOffset: 0
+            yOffset: 0,
+            editQuota: true
         };
     },
     methods: {
@@ -549,6 +641,9 @@ export default {
             if (data.model3d) {
                 data.model3d.url = data.model3d.url + '?scale=' + this.scale + '&yOffset=' + this.yOffset;
             }
+            if (this.editQuota && data.totalQuota) {
+                data.vipQuota = data.totalQuota;
+            }
 
             this.saving = true;
             this.$http
@@ -676,4 +771,7 @@ export default {
         display: inline-block;
     }
 }
+.right-margin {
+    margin-left: 50px;
+}
 </style>

+ 31 - 0
src/test/java/com/izouma/nineth/repo/CollectionRepoTest.java

@@ -0,0 +1,31 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.ApplicationTests;
+import com.izouma.nineth.service.AssetService;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class CollectionRepoTest extends ApplicationTests {
+    @Autowired
+    private CollectionRepo collectionRepo;
+    @Autowired
+    private AssetService   assetService;
+
+    @Test
+    public void updateCDN() {
+        List<List<String>> list = collectionRepo.selectResource(83346L);
+        for (int i = 0; i < list.get(0).size(); i++) {
+            list.get(0).set(i, assetService.replaceCDN(list.get(0).get(i)));
+        }
+        collectionRepo.updateCDN(Long.parseLong(list.get(0).get(0)),
+                list.get(0).get(1),
+                list.get(0).get(2),
+                list.get(0).get(3),
+                list.get(0).get(4),
+                list.get(0).get(5));
+    }
+}

+ 5 - 0
src/test/java/com/izouma/nineth/repo/UserRepoTest.java

@@ -51,4 +51,9 @@ public class UserRepoTest {
                 .build());
         System.out.println(list);
     }
+
+    @Test
+    public void test() {
+        System.out.println(userRepo.findById(150843L).orElse(null));
+    }
 }

+ 7 - 0
src/test/java/com/izouma/nineth/service/AdapayMerchantServiceTest.java

@@ -1,9 +1,16 @@
 package com.izouma.nineth.service;
 
 import com.izouma.nineth.ApplicationTests;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
 
 import static org.junit.Assert.*;
 
 public class AdapayMerchantServiceTest extends ApplicationTests {
+    @Autowired
+    private AdapayMerchantService adapayMerchantService;
 
+    @Test
+    public void createSettleAccountForAll() {
+    }
 }

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

@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -165,6 +166,7 @@ class AssetServiceTest extends ApplicationTests {
         System.out.println(asset);
     }
 
+
     @Data
     @NoArgsConstructor
     @AllArgsConstructor
@@ -235,4 +237,9 @@ class AssetServiceTest extends ApplicationTests {
             rocketMQTemplate.syncSend("mint-topic", asset.getId());
         }
     }
+
+    @Test
+    public void transferCDN() throws ExecutionException, InterruptedException {
+        assetService.transferCDN();
+    }
 }

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

@@ -244,8 +244,8 @@ public class OrderServiceTest extends ApplicationTests {
 
     @Test
     public void test() {
-        orderService.create(9850L, 196308L, 1, null, null, null,
-                945378720611303424L, true, 2);
+        orderService.create(9972L, 83346L, 1, null, null, null,
+                112342311L, false);
     }
 
     @Test

+ 66 - 3
src/test/java/com/izouma/nineth/service/UserServiceTest.java

@@ -1,16 +1,26 @@
 package com.izouma.nineth.service;
 
 import com.github.kevinsawicki.http.HttpRequest;
+import com.huifu.adapay.core.exception.BaseAdaPayException;
 import com.izouma.nineth.ApplicationTests;
 import com.izouma.nineth.config.Constants;
+import com.izouma.nineth.domain.IdentityAuth;
 import com.izouma.nineth.domain.User;
+import com.izouma.nineth.dto.BankValidate;
 import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.dto.UserBankCard;
 import com.izouma.nineth.dto.UserRegister;
+import com.izouma.nineth.enums.AuthStatus;
 import com.izouma.nineth.enums.AuthorityName;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.IdentityAuthRepo;
+import com.izouma.nineth.repo.UserBankCardRepo;
 import com.izouma.nineth.repo.UserRepo;
 import com.izouma.nineth.security.Authority;
 import com.izouma.nineth.service.storage.StorageService;
+import com.izouma.nineth.utils.BankUtils;
 import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -24,11 +34,17 @@ import java.util.Map;
 public class UserServiceTest extends ApplicationTests {
 
     @Autowired
-    private UserService    userService;
+    private UserService           userService;
     @Autowired
-    private UserRepo       userRepo;
+    private UserRepo              userRepo;
     @Autowired
-    private StorageService storageService;
+    private StorageService        storageService;
+    @Autowired
+    private UserBankCardRepo      userBankCardRepo;
+    @Autowired
+    private AdapayMerchantService adapayMerchantService;
+    @Autowired
+    private IdentityAuthRepo      identityAuthRepo;
 
     @Test
     public void findByUsernameAndDelFalse1() {
@@ -122,4 +138,51 @@ public class UserServiceTest extends ApplicationTests {
     public void switchAccount() {
         userService.checkSettleAccount();
     }
+
+    @Test
+    public void phoneRegister() {
+        userService.phoneRegister("18100004444", "1234", "123456", null, 9972L, 206925L);
+    }
+
+    @Test
+    public void addBankCard() throws BaseAdaPayException {
+        Long userId = 134613L;
+        String bankNo = "6222024301070380165";
+        String phone = "15077886171";
+        User user = userRepo.findById(userId).orElseThrow(new BusinessException("用户不存在"));
+        IdentityAuth identityAuth = identityAuthRepo.findFirstByUserIdAndStatusAndDelFalseOrderByCreatedAtDesc(userId, AuthStatus.SUCCESS)
+                .orElseThrow(new BusinessException("用户未认证"));
+        if (identityAuth.isOrg()) {
+            //throw new BusinessException("企业认证用户请绑定对公账户");
+        }
+        if (!StringUtils.isBlank(user.getSettleAccountId())) {
+            throw new BusinessException("此账号已绑定");
+        }
+        BankValidate bankValidate = BankUtils.validate(bankNo);
+        if (!bankValidate.isValidated()) {
+            throw new BusinessException("暂不支持此卡");
+        }
+
+        adapayMerchantService.createMemberForAll(userId.toString(), user.getPhone(), identityAuth.getRealName(), identityAuth.getIdNo());
+        user.setMemberId(user.getId().toString());
+        userRepo.save(user);
+
+        String accountId = adapayMerchantService.createSettleAccountForAll
+                (user.getMemberId(), identityAuth.getRealName(),
+                        identityAuth.getIdNo(), phone, bankNo);
+        user.setSettleAccountId(accountId);
+        userRepo.save(user);
+
+        userBankCardRepo.save(UserBankCard.builder()
+                .bank(bankValidate.getBank())
+                .bankName(bankValidate.getBankName())
+                .bankNo(bankNo)
+                .cardType(bankValidate.getCardType())
+                .cardTypeDesc(bankValidate.getCardTypeDesc())
+                .userId(userId)
+                .phone(phone)
+                .realName(identityAuth.getRealName())
+                .idNo(identityAuth.getIdNo())
+                .build());
+    }
 }