xiongzhu 3 лет назад
Родитель
Сommit
8d2808bdba
32 измененных файлов с 951 добавлено и 45 удалено
  1. 0 7
      src/main/java/com/izouma/nineth/JsonView/UserView.java
  2. 3 3
      src/main/java/com/izouma/nineth/config/CacheConfig.java
  3. 32 0
      src/main/java/com/izouma/nineth/converter/MintRuleConverter.java
  4. 28 0
      src/main/java/com/izouma/nineth/domain/Asset.java
  5. 45 0
      src/main/java/com/izouma/nineth/domain/Collection.java
  6. 7 0
      src/main/java/com/izouma/nineth/domain/MintActivity.java
  7. 28 0
      src/main/java/com/izouma/nineth/domain/Tag.java
  8. 19 0
      src/main/java/com/izouma/nineth/domain/TestClass.java
  9. 9 3
      src/main/java/com/izouma/nineth/domain/User.java
  10. 15 0
      src/main/java/com/izouma/nineth/dto/MintActivityRule.java
  11. 12 0
      src/main/java/com/izouma/nineth/dto/MintActivityRuleDetail.java
  12. 12 0
      src/main/java/com/izouma/nineth/repo/TagRepo.java
  13. 9 0
      src/main/java/com/izouma/nineth/service/AssetService.java
  14. 87 13
      src/main/java/com/izouma/nineth/service/CollectionService.java
  15. 59 4
      src/main/java/com/izouma/nineth/service/MintActivityService.java
  16. 20 0
      src/main/java/com/izouma/nineth/service/TagService.java
  17. 41 0
      src/main/java/com/izouma/nineth/utils/CollectionTagSerializer.java
  18. 3 0
      src/main/java/com/izouma/nineth/web/AssetController.java
  19. 8 4
      src/main/java/com/izouma/nineth/web/CollectionController.java
  20. 2 1
      src/main/java/com/izouma/nineth/web/FileUploadController.java
  21. 62 0
      src/main/java/com/izouma/nineth/web/TagController.java
  22. 5 2
      src/main/java/com/izouma/nineth/web/TestClassController.java
  23. 1 1
      src/main/resources/application.yaml
  24. 1 0
      src/main/resources/genjson/Tag.json
  25. 44 0
      src/main/vue/src/components/TagSelect.vue
  26. 18 2
      src/main/vue/src/router.js
  27. 5 1
      src/main/vue/src/views/CollectionEdit.vue
  28. 110 0
      src/main/vue/src/views/TagEdit.vue
  29. 178 0
      src/main/vue/src/views/TagList.vue
  30. 55 4
      src/test/java/com/izouma/nineth/CommonTest.java
  31. 26 0
      src/test/java/com/izouma/nineth/TestClassTest.java
  32. 7 0
      src/test/java/com/izouma/nineth/repo/CollectionRepoTest.java

+ 0 - 7
src/main/java/com/izouma/nineth/JsonView/UserView.java

@@ -1,7 +0,0 @@
-package com.izouma.nineth.JsonView;
-
-public interface UserView {
-    public static class Redis {
-    }
-
-}

+ 3 - 3
src/main/java/com/izouma/nineth/config/CacheConfig.java

@@ -10,7 +10,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.izouma.nineth.JsonView.UserView;
+import com.izouma.nineth.domain.User;
 import org.springframework.boot.autoconfigure.AutoConfigureAfter;
 import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
 import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
@@ -54,7 +54,7 @@ public class CacheConfig {
                 .enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING));
         mapper.registerModule(new JavaTimeModule());
         mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
-        mapper.setConfig(mapper.getSerializationConfig().withView(UserView.Redis.class));
+        mapper.setConfig(mapper.getSerializationConfig().withView(User.View.Detail.class));
         SimpleModule simpleModule = new SimpleModule();
         simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
         simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
@@ -92,7 +92,7 @@ public class CacheConfig {
                 .enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING));
         mapper.registerModule(new JavaTimeModule());
         mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
-        mapper.setConfig(mapper.getSerializationConfig().withView(UserView.Redis.class));
+        mapper.setConfig(mapper.getSerializationConfig().withView(User.View.Detail.class));
         SimpleModule simpleModule = new SimpleModule();
         simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
         simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);

+ 32 - 0
src/main/java/com/izouma/nineth/converter/MintRuleConverter.java

@@ -0,0 +1,32 @@
+package com.izouma.nineth.converter;
+
+import com.alibaba.fastjson.JSON;
+import com.izouma.nineth.dto.MintActivityRule;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.persistence.AttributeConverter;
+
+public class MintRuleConverter implements AttributeConverter<MintActivityRule, String> {
+    @Override
+    public String convertToDatabaseColumn(MintActivityRule mintActivityRule) {
+        if (mintActivityRule == null) {
+            return null;
+        } else {
+            return JSON.toJSONString(mintActivityRule);
+        }
+    }
+
+    @Override
+    public MintActivityRule convertToEntityAttribute(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        } else {
+            try {
+                return JSON.parseObject(s, MintActivityRule.class);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+}

+ 28 - 0
src/main/java/com/izouma/nineth/domain/Asset.java

@@ -1,5 +1,7 @@
 package com.izouma.nineth.domain;
 
+import com.fasterxml.jackson.annotation.JsonView;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.izouma.nineth.annotations.Searchable;
 import com.izouma.nineth.converter.FileObjectConverter;
 import com.izouma.nineth.converter.FileObjectListConverter;
@@ -7,17 +9,21 @@ import com.izouma.nineth.converter.PrivilegeListConverter;
 import com.izouma.nineth.converter.PropertyListConverter;
 import com.izouma.nineth.enums.AssetStatus;
 import com.izouma.nineth.enums.CollectionType;
+import com.izouma.nineth.utils.CollectionTagSerializer;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.hibernate.annotations.DynamicUpdate;
 
 import javax.persistence.*;
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 
 @Data
@@ -29,12 +35,21 @@ import java.util.List;
         @Index(columnList = "minterId"),
         @Index(columnList = "ownerId"),
 })
+@DynamicUpdate
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder
 @ApiModel("资产")
 public class Asset extends CollectionBaseEntity {
 
+    public interface View {
+        interface Basic {
+        }
+
+        interface Detail {
+        }
+    }
+
     @ApiModelProperty("用户ID")
     private Long userId;
 
@@ -167,6 +182,18 @@ public class Asset extends CollectionBaseEntity {
     @Transient
     private boolean opened = true;
 
+    @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL})
+    @JoinTable(
+            name = "asset_tag",
+            joinColumns = {@JoinColumn(name = "asset_id", referencedColumnName = "id", foreignKey = @ForeignKey(
+                    foreignKeyDefinition = "FOREIGN KEY (asset_id) REFERENCES asset(id) ON DELETE CASCADE"))},
+            inverseJoinColumns = {@JoinColumn(name = "tag_id", referencedColumnName = "id", foreignKey = @ForeignKey(
+                    foreignKeyDefinition = "FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE"
+            ))})
+    @JsonSerialize(using = CollectionTagSerializer.class, as = HashSet.class)
+    @JsonView(View.Detail.class)
+    private Set<Tag> tags = new HashSet<>();
+
     public static Asset create(Collection collection, User user) {
         return Asset.builder()
                 .userId(user.getId())
@@ -191,6 +218,7 @@ public class Asset extends CollectionBaseEntity {
                 .ownerAvatar(user.getAvatar())
                 .type(collection.getType())
                 .holdDays(collection.getHoldDays())
+                .tags(collection.getTags())
                 .build();
     }
 

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

@@ -1,5 +1,9 @@
 package com.izouma.nineth.domain;
 
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonView;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.izouma.nineth.annotations.Searchable;
 import com.izouma.nineth.annotations.SearchableOne;
 import com.izouma.nineth.converter.FileObjectConverter;
@@ -8,18 +12,25 @@ import com.izouma.nineth.converter.PrivilegeListConverter;
 import com.izouma.nineth.converter.PropertyListConverter;
 import com.izouma.nineth.enums.CollectionSource;
 import com.izouma.nineth.enums.CollectionType;
+import com.izouma.nineth.security.Authority;
+import com.izouma.nineth.utils.CollectionTagSerializer;
+import com.izouma.nineth.utils.UserAuthoritySerializer;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.hibernate.annotations.BatchSize;
+import org.hibernate.annotations.DynamicUpdate;
 import org.hibernate.annotations.Formula;
 
 import javax.persistence.*;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 @Data
 @Entity
@@ -44,12 +55,30 @@ import java.util.List;
         @Index(columnList = "onShelf,del,source,salable"),
         @Index(columnList = "onShelf,del,source,salable,price"),
 })
+@DynamicUpdate
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder
 @ApiModel("藏品")
 public class Collection extends CollectionBaseEntity {
 
+    public interface View {
+        interface Basic {
+        }
+
+        interface Internal {
+        }
+    }
+
+    @Transient
+    private boolean liked;
+
+    @Transient
+    private boolean appointment;
+
+    @Transient
+    private int vipSurplus;
+
     @ApiModelProperty("名称")
     @Searchable
     private String name;
@@ -214,4 +243,20 @@ public class Collection extends CollectionBaseEntity {
 
     @ApiModelProperty("最低消费")
     private BigDecimal minimumCharge;
+
+    @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
+    @JoinTable(
+            name = "collection_tag",
+            joinColumns = {@JoinColumn(name = "collection_id", referencedColumnName = "id",
+                    foreignKey = @ForeignKey(foreignKeyDefinition =
+                            "FOREIGN KEY (collection_id) REFERENCES collection_info(id) ON DELETE CASCADE ON UPDATE CASCADE"
+                    ))},
+            inverseJoinColumns = {@JoinColumn(name = "tag_id", referencedColumnName = "id",
+                    foreignKey = @ForeignKey(foreignKeyDefinition =
+                            "FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE"
+                    ))})
+    @JsonSerialize(using = CollectionTagSerializer.class, as = HashSet.class)
+    @JsonView(View.Internal.class)
+    private Set<Tag> tags = new HashSet<>();
+
 }

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

@@ -1,7 +1,9 @@
 package com.izouma.nineth.domain;
 
 import com.izouma.nineth.annotations.Searchable;
+import com.izouma.nineth.converter.MintRuleConverter;
 import com.izouma.nineth.converter.StringArrayConverter;
+import com.izouma.nineth.dto.MintActivityRule;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
@@ -78,4 +80,9 @@ public class MintActivity extends BaseEntity {
     @Column(columnDefinition = "tinyint unsigned default 0")
     @ApiModelProperty("是否审核")
     private boolean audit = false;
+
+    @ApiModelProperty("铸造活动规则")
+    @Convert(converter = MintRuleConverter.class)
+    @Column(columnDefinition = "TEXT")
+    private MintActivityRule rule;
 }

+ 28 - 0
src/main/java/com/izouma/nineth/domain/Tag.java

@@ -0,0 +1,28 @@
+package com.izouma.nineth.domain;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.*;
+
+import javax.persistence.*;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(value = {"hibernateLazyInitializer"}, ignoreUnknown = true)
+@EqualsAndHashCode(callSuper = false)
+public class Tag {
+
+    @ExcelProperty("ID")
+    @Id
+    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_sequence")
+    private Long id;
+
+    private String name;
+
+    private String remark;
+}

+ 19 - 0
src/main/java/com/izouma/nineth/domain/TestClass.java

@@ -1,9 +1,11 @@
 package com.izouma.nineth.domain;
 
+import com.fasterxml.jackson.annotation.JsonView;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.hibernate.annotations.DynamicUpdate;
 
 import javax.persistence.Entity;
 import javax.persistence.GeneratedValue;
@@ -15,7 +17,16 @@ import javax.persistence.Id;
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder
+@DynamicUpdate
 public class TestClass {
+    public interface View {
+        interface Basic {
+        }
+
+        interface Internal extends Basic {
+        }
+    }
+
     public TestClass(String name) {
         this.name = name;
     }
@@ -25,4 +36,12 @@ public class TestClass {
     private Long id;
 
     private String name;
+
+    private String columnA;
+
+    private String columnB;
+
+    @JsonView(View.Internal.class)
+    private String columnC;
+
 }

+ 9 - 3
src/main/java/com/izouma/nineth/domain/User.java

@@ -4,7 +4,6 @@ import com.alibaba.excel.annotation.ExcelIgnore;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonView;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import com.izouma.nineth.JsonView.UserView;
 import com.izouma.nineth.annotations.Searchable;
 import com.izouma.nineth.enums.AuthStatus;
 import com.izouma.nineth.enums.SearchMode;
@@ -16,7 +15,6 @@ import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import net.bytebuddy.implementation.bind.annotation.SuperCall;
 import org.hibernate.annotations.BatchSize;
 
 import javax.persistence.*;
@@ -46,6 +44,14 @@ import java.util.Set;
 @ApiModel(value = "用户", description = "用户")
 public class User extends UserBaseEntity implements Serializable {
 
+    public interface View {
+        interface Basic {
+        }
+
+        interface Detail {
+        }
+    }
+
     //    @Pattern(regexp = Constants.Regex.USERNAME)
     @Size(min = 1, max = 50)
     @Column(nullable = false, unique = true)
@@ -57,7 +63,7 @@ public class User extends UserBaseEntity implements Serializable {
 
     private String avatar;
 
-    @JsonView(UserView.Redis.class)
+    @JsonView(View.Detail.class)
     private String password;
 
     private boolean del = false;

+ 15 - 0
src/main/java/com/izouma/nineth/dto/MintActivityRule.java

@@ -0,0 +1,15 @@
+package com.izouma.nineth.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class MintActivityRule {
+
+    private MintActivityRuleDetail detail;
+
+    private List<MintActivityRule> and;
+
+    private List<MintActivityRule> or;
+}

+ 12 - 0
src/main/java/com/izouma/nineth/dto/MintActivityRuleDetail.java

@@ -0,0 +1,12 @@
+package com.izouma.nineth.dto;
+
+import com.izouma.nineth.domain.Tag;
+import lombok.Data;
+
+@Data
+public class MintActivityRuleDetail {
+
+    private Tag tag;
+
+    private int num;
+}

+ 12 - 0
src/main/java/com/izouma/nineth/repo/TagRepo.java

@@ -0,0 +1,12 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.Tag;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import javax.transaction.Transactional;
+
+public interface TagRepo extends JpaRepository<Tag, Long>, JpaSpecificationExecutor<Tag> {
+}

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

@@ -103,11 +103,20 @@ public class AssetService {
 
     public Asset createAsset(BlindBoxItem winItem, User user, Long orderId, BigDecimal price, String type,
                              Integer number, Integer holdDays) {
+        Collection blindBox = collectionRepo.findById(winItem.getBlindBoxId()).orElseThrow(new BusinessException("盲盒不存在"));
+        Collection collection = collectionRepo.findById(winItem.getCollectionId()).orElseThrow(new BusinessException("藏品不存在"));
         Asset asset = Asset.create(winItem, user, holdDays);
         asset.setTokenId(TokenUtils.genTokenId());
         asset.setNumber(number);
         asset.setOrderId(orderId);
         asset.setPrice(price);
+        asset.setTags(new HashSet<>());
+        if (blindBox.getTags() != null) {
+            asset.getTags().addAll(blindBox.getTags());
+        }
+        if (collection.getTags() != null) {
+            asset.getTags().addAll(collection.getTags());
+        }
         assetRepo.saveAndFlush(asset);
 
         tokenHistoryRepo.save(TokenHistory.builder()

+ 87 - 13
src/main/java/com/izouma/nineth/service/CollectionService.java

@@ -70,6 +70,7 @@ public class CollectionService {
     private Environment                   env;
     private OrderRepo                     orderRepo;
     private TokenHistoryRepo              tokenHistoryRepo;
+    private TagRepo                       tagRepo;
 
     private final Map<Long, ScheduledFuture<?>> tasks = new HashMap<>();
 
@@ -131,6 +132,10 @@ public class CollectionService {
         record.setStock(record.getTotal());
         record.setSale(0);
         record.setVipQuota(record.getTotalQuota());
+        if (!record.getTags().isEmpty()) {
+            record.setTags(new HashSet<>(tagRepo.findAllById(record.getTags().stream().map(Tag::getId)
+                    .collect(Collectors.toList()))));
+        }
         if (record.isScheduleSale()) {
             if (record.getStartTime() == null) {
                 throw new BusinessException("请填写定时发布时间");
@@ -149,18 +154,50 @@ public class CollectionService {
     }
 
     public Collection update(Collection record) {
-        collectionRepo.update(record.getId(), record.isOnShelf(), record.isSalable(),
-                record.getStartTime(), record.isScheduleSale(), record.getSort(),
-                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.getVipQuota(), record.getTimeDelay(), record.getSaleTime(), record.getHoldDays(),
-                record.getOpenQuota(), record.getTotalQuota(), record.getMinimumCharge());
-
-        record = collectionRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
-        onShelfTask(record);
-        return record;
+//        collectionRepo.update(record.getId(), record.isOnShelf(), record.isSalable(),
+//                record.getStartTime(), record.isScheduleSale(), record.getSort(),
+//                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.getVipQuota(), record.getTimeDelay(), record.getSaleTime(), record.getHoldDays(),
+//                record.getOpenQuota(), record.getTotalQuota(), record.getMinimumCharge());
+
+        Collection collection = collectionRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+        collection.setOnShelf(record.isOnShelf());
+        collection.setSalable(record.isSalable());
+        collection.setStartTime(record.getStartTime());
+        collection.setScheduleSale(record.isScheduleSale());
+        collection.setSort(record.getSort());
+        collection.setDetail(record.getDetail());
+        collection.setPrivileges(record.getPrivileges());
+        collection.setProperties(record.getProperties());
+        collection.setModel3d(record.getModel3d());
+        collection.setMaxCount(record.getMaxCount());
+        collection.setCountId(record.getCountId());
+        collection.setScanCode(record.isScanCode());
+        collection.setNoSoldOut(record.isNoSoldOut());
+        collection.setAssignment(record.getAssignment());
+        collection.setCouponPayment(record.isCouponPayment());
+        collection.setShareBg(record.getShareBg());
+        collection.setRegisterBg(record.getRegisterBg());
+        collection.setVipQuota(record.getVipQuota());
+        collection.setTimeDelay(record.getTimeDelay());
+        collection.setSaleTime(record.getSaleTime());
+        collection.setHoldDays(record.getHoldDays());
+        collection.setOpenQuota(record.getOpenQuota());
+        collection.setTotalQuota(record.getTotalQuota());
+        collection.setMinimumCharge(record.getMinimumCharge());
+        if (record.getTags().isEmpty()) {
+            collection.setTags(null);
+        } else {
+            collection.setTags(new HashSet<>(tagRepo.findAllById(record.getTags().stream().map(Tag::getId)
+                    .collect(Collectors.toList()))));
+        }
+        collection = collectionRepo.save(collection);
+
+        onShelfTask(collection);
+        return collection;
     }
 
     private void onShelfTask(Collection record) {
@@ -209,6 +246,25 @@ public class CollectionService {
         return collectionDTO;
     }
 
+    public Collection queryUserDetail(Collection collection, boolean join, boolean showVip) {
+        if (join) {
+            User user = SecurityUtils.getAuthenticatedUser();
+            if (user != null) {
+                List<Like> list = likeRepo.findByUserIdAndCollectionId(user.getId(),
+                        collection.getId());
+                collection.setLiked(!list.isEmpty());
+                if (collection.getType() == CollectionType.BLIND_BOX) {
+                    collection.setAppointment(appointmentRepo.findFirstByBlindBoxId(collection.getId()).isPresent());
+                }
+                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));
+                    collection.setVipSurplus(user.getVipPurchase() - purchase);
+                }
+            }
+        }
+        return collection;
+    }
+
     public List<CollectionDTO> toDTO(List<Collection> collections) {
         List<Like> likes = new ArrayList<>();
         List<Appointment> appointments = new ArrayList<>();
@@ -228,6 +284,24 @@ public class CollectionService {
         }).collect(Collectors.toList());
     }
 
+    public void queryUserDetail(List<Collection> collections) {
+        List<Like> likes = new ArrayList<>();
+        List<Appointment> appointments = new ArrayList<>();
+        if (SecurityUtils.getAuthenticatedUser() != null) {
+            likes.addAll(likeRepo.findByUserIdAndCollectionIdIsNotNull(SecurityUtils.getAuthenticatedUser().getId()));
+            appointments.addAll(appointmentRepo.findByUserId(SecurityUtils.getAuthenticatedUser().getId()));
+        }
+        collections.stream().parallel().forEach(collection -> {
+            queryUserDetail(collection, false, false);
+            if (!likes.isEmpty()) {
+                collection.setLiked(likes.stream().anyMatch(l -> l.getCollectionId().equals(collection.getId())));
+            }
+            if (!appointments.isEmpty()) {
+                collection.setAppointment(appointments.stream().anyMatch(a -> a.getBlindBoxId().equals(collection.getId())));
+            }
+        });
+    }
+
     public Page<CollectionDTO> toDTO(Page<Collection> collections) {
         List<CollectionDTO> userDTOS = toDTO(collections.getContent());
         return new PageImpl<>(userDTOS, collections.getPageable(), collections.getTotalElements());
@@ -416,7 +490,7 @@ public class CollectionService {
         return increaseSale(id, -number);
     }
 
-//    @Debounce(key = "#id", delay = 500)
+    //    @Debounce(key = "#id", delay = 500)
     public void syncStock(Long id) {
         Integer stock = (Integer) redisTemplate.opsForValue().get(RedisKeys.COLLECTION_STOCK + id);
         if (stock != null) {

+ 59 - 4
src/main/java/com/izouma/nineth/service/MintActivityService.java

@@ -3,13 +3,19 @@ package com.izouma.nineth.service;
 import com.izouma.nineth.annotations.Debounce;
 import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.config.RedisKeys;
+import com.izouma.nineth.domain.Asset;
 import com.izouma.nineth.domain.Collection;
 import com.izouma.nineth.domain.MintActivity;
+import com.izouma.nineth.domain.Tag;
+import com.izouma.nineth.dto.MintActivityRule;
+import com.izouma.nineth.dto.MintActivityRuleDetail;
 import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.MintActivityRepo;
 import com.izouma.nineth.utils.JpaUtils;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.data.domain.Page;
 import org.springframework.data.redis.core.BoundValueOperations;
@@ -19,12 +25,10 @@ import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 @Slf4j
 @Service
@@ -106,4 +110,55 @@ public class MintActivityService {
         }
     }
 
+    public void checkRule(MintActivityRule rule) {
+        if (CollectionUtils.isNotEmpty(rule.getAnd()) && CollectionUtils.isNotEmpty(rule.getOr())) {
+            throw new BusinessException("铸造活动规则不能同时使用and和or");
+        }
+        if (rule.getDetail() != null &&
+                (CollectionUtils.isNotEmpty(rule.getAnd()) || CollectionUtils.isNotEmpty(rule.getOr()))) {
+            throw new BusinessException("铸造活动规则不能同时使用detail和and/or");
+        }
+    }
+
+    public boolean matchRule(List<Asset> assets, MintActivityRule rule) {
+        checkRule(rule);
+        if (rule.getDetail() != null) {
+            MintActivityRuleDetail detail = rule.getDetail();
+            Tag tag = detail.getTag();
+            int num = detail.getNum();
+
+            List<Asset> matched = assets.stream().filter(asset -> asset.getTags().stream()
+                    .anyMatch(t -> t.getId().equals(tag.getId()))).collect(Collectors.toList());
+            if (matched.size() >= num) {
+                for (int i = 0; i < num; i++) {
+                    Long id = matched.get(i).getId();
+                    assets.removeIf(asset -> asset.getId().equals(id));
+                }
+                return true;
+            }
+        } else if (CollectionUtils.isNotEmpty(rule.getAnd())) {
+            return matchAnd(assets, rule.getAnd());
+        } else if (CollectionUtils.isNotEmpty(rule.getOr())) {
+            return matchOr(assets, rule.getOr());
+        }
+        return false;
+    }
+
+    public boolean matchAnd(List<Asset> assets, List<MintActivityRule> and) {
+        for (MintActivityRule rule : and) {
+            if (!matchRule(assets, rule)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public boolean matchOr(List<Asset> assets, List<MintActivityRule> or) {
+        for (MintActivityRule rule : or) {
+            if (matchRule(assets, rule)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

+ 20 - 0
src/main/java/com/izouma/nineth/service/TagService.java

@@ -0,0 +1,20 @@
+package com.izouma.nineth.service;
+
+import com.izouma.nineth.domain.Tag;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.repo.TagRepo;
+import com.izouma.nineth.utils.JpaUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class TagService {
+
+    private TagRepo tagRepo;
+
+    public Page<Tag> all(PageQuery pageQuery) {
+        return tagRepo.findAll(JpaUtils.toSpecification(pageQuery, Tag.class), JpaUtils.toPageRequest(pageQuery));
+    }
+}

+ 41 - 0
src/main/java/com/izouma/nineth/utils/CollectionTagSerializer.java

@@ -0,0 +1,41 @@
+package com.izouma.nineth.utils;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.type.WritableTypeId;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+import com.izouma.nineth.domain.Tag;
+import com.izouma.nineth.security.Authority;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+public class CollectionTagSerializer extends JsonSerializer<Set<Tag>> {
+    @Override
+    public void serialize(Set<Tag> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        if (value == null) {
+            gen.writeNull();
+        } else {
+            gen.writeStartArray();
+            for (Tag tag : value) {
+                gen.writeObject(tag);
+            }
+            gen.writeEndArray();
+        }
+    }
+
+    @Override
+    public void serializeWithType(Set<Tag> value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
+        HashSet<Tag> set = new HashSet<>(value);
+        WritableTypeId typeId = typeSer.writeTypePrefix(gen, typeSer.typeId(set, JsonToken.VALUE_STRING));
+        gen.writeStartArray();
+        for (Tag tag : set) {
+            gen.writeObject(tag);
+        }
+        gen.writeEndArray();
+        typeSer.writeTypeSuffix(gen, typeId);
+    }
+}

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

@@ -1,5 +1,6 @@
 package com.izouma.nineth.web;
 
+import com.fasterxml.jackson.annotation.JsonView;
 import com.izouma.nineth.TokenHistory;
 import com.izouma.nineth.domain.Asset;
 import com.izouma.nineth.domain.GiftOrder;
@@ -49,12 +50,14 @@ public class AssetController extends BaseController {
 
     //@PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/all")
+    @JsonView(Asset.View.Basic.class)
     public Page<Asset> all(@RequestBody PageQuery pageQuery) {
         pageQuery.getQuery().put("userId", SecurityUtils.getAuthenticatedUser().getId());
         return assetService.all(pageQuery);
     }
 
     @GetMapping("/get/{id}")
+    @JsonView(Asset.View.Basic.class)
     public Asset get(@PathVariable Long id) {
         Asset asset = assetRepo.findById(id).orElseThrow(new BusinessException("无记录"));
         orderRepo.findByIdAndDelFalse(asset.getOrderId()).ifPresent(order -> asset.setOpened(order.isOpened()));

+ 8 - 4
src/main/java/com/izouma/nineth/web/CollectionController.java

@@ -1,5 +1,6 @@
 package com.izouma.nineth.web;
 
+import com.fasterxml.jackson.annotation.JsonView;
 import com.izouma.nineth.domain.Collection;
 import com.izouma.nineth.domain.CollectionPrivilege;
 import com.izouma.nineth.domain.FileObject;
@@ -75,13 +76,16 @@ public class CollectionController extends BaseController {
 
     //@PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/all")
-    public Page<CollectionDTO> all(@RequestBody PageQuery pageQuery) {
-        return collectionService.toDTO(collectionService.all(pageQuery).toPage());
+    @JsonView(Collection.View.Basic.class)
+    public Page<Collection> all(@RequestBody PageQuery pageQuery) {
+        Page<Collection> page = collectionService.all(pageQuery).toPage();
+        collectionService.queryUserDetail(page.getContent());
+        return page;
     }
 
     @GetMapping("/get/{id}")
-    public CollectionDTO get(@PathVariable Long id) {
-        return collectionService.toDTO(collectionRepo.findById(id)
+    public Collection get(@PathVariable Long id) {
+        return collectionService.queryUserDetail(collectionRepo.findById(id)
                 .orElseThrow(new BusinessException("无记录")), true, true);
     }
 

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

@@ -47,7 +47,7 @@ public class FileUploadController {
                              @RequestParam(value = "compress", defaultValue = "false") boolean compress,
                              @RequestParam(value = "width", required = false) Integer width,
                              @RequestParam(value = "height", required = false) Integer height) throws IOException {
-
+        if ("aarch64".equals(System.getProperty("os.arch"))) compress = false;
         String ext = Optional.ofNullable(FilenameUtils.getExtension(file.getOriginalFilename())).orElse("")
                 .toLowerCase().replace("jpeg", "jpg");
         if (path == null) {
@@ -112,6 +112,7 @@ public class FileUploadController {
                                        @RequestParam(value = "compress", defaultValue = "false") boolean compress,
                                        @RequestParam(value = "width", required = false) Integer width,
                                        @RequestParam(value = "height", required = false) Integer height) throws IOException {
+        if ("aarch64".equals(System.getProperty("os.arch"))) compress = false;
         String ext = Optional.ofNullable(FilenameUtils.getExtension(file.getOriginalFilename())).orElse("")
                 .toLowerCase().replace("jpeg", "jpg");
         String[] extList = new String[]{"jpg", "jpeg", "png", "gif", "mp4"};

+ 62 - 0
src/main/java/com/izouma/nineth/web/TagController.java

@@ -0,0 +1,62 @@
+package com.izouma.nineth.web;
+
+import com.izouma.nineth.domain.Tag;
+import com.izouma.nineth.service.TagService;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.TagRepo;
+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;
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/tag")
+@AllArgsConstructor
+public class TagController extends BaseController {
+    private TagService tagService;
+    private TagRepo    tagRepo;
+
+    @PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public Tag save(@RequestBody Tag record) {
+        if (record.getId() != null) {
+            Tag orig = tagRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+            ObjUtils.merge(orig, record);
+            return tagRepo.save(orig);
+        }
+        return tagRepo.save(record);
+    }
+
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/all")
+    public Page<Tag> all(@RequestBody PageQuery pageQuery) {
+        return tagService.all(pageQuery);
+    }
+
+    @GetMapping("/get/{id}")
+    public Tag get(@PathVariable Long id) {
+        return tagRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    @PreAuthorize("hasRole('ADMIN')")
+    public void del(@PathVariable Long id) {
+        tagRepo.deleteById(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<Tag> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+}
+

+ 5 - 2
src/main/java/com/izouma/nineth/web/TestClassController.java

@@ -1,5 +1,6 @@
 package com.izouma.nineth.web;
 
+import com.fasterxml.jackson.annotation.JsonView;
 import com.izouma.nineth.domain.TestClass;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.exception.BusinessException;
@@ -41,12 +42,14 @@ public class TestClassController extends BaseController {
     }
 
     //@PreAuthorize("hasRole('ADMIN')")
-    @PostMapping("/all")
-    public Page<TestClass> all(@RequestBody PageQuery pageQuery) {
+    @GetMapping("/all")
+    @JsonView(TestClass.View.Basic.class)
+    public Page<TestClass> all(PageQuery pageQuery) {
         return testClassService.all(pageQuery);
     }
 
     @GetMapping("/get/{id}")
+    @JsonView(TestClass.View.Basic.class)
     public TestClass get(@PathVariable Long id) {
         return testClassRepo.findById(id).orElseThrow(new BusinessException("无记录"));
     }

+ 1 - 1
src/main/resources/application.yaml

@@ -27,7 +27,7 @@ spring:
   datasource:
 #    url: jdbc:mysql://rm-wz9sc79f5255780opqo.mysql.rds.aliyuncs.com/raex?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8
 #    username: raex_server
-    url: jdbc:mysql://rm-wz9q65wzuf8c56647ro.mysql.rds.aliyuncs.com/raex_test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8
+    url: jdbc:mysql://rm-wz9q65wzuf8c56647ro.mysql.rds.aliyuncs.com/raex_dev?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8
     username: raex_test
     password: tetQsjw!u4!c5$URduo7BH
     hikari:

+ 1 - 0
src/main/resources/genjson/Tag.json

@@ -0,0 +1 @@
+{"tableName":"Tag","className":"Tag","remark":"标签","genTable":true,"genClass":true,"genList":true,"genForm":true,"genRouter":true,"javaPath":"/Users/drew/Projects/Java/raex_back/src/main/java/com/izouma/nineth","viewPath":"/Users/drew/Projects/Java/raex_back/src/main/vue/src/views","routerPath":"/Users/drew/Projects/Java/raex_back/src/main/vue/src","resourcesPath":"/Users/drew/Projects/Java/raex_back/src/main/resources","dataBaseType":"Mysql","fields":[{"name":"name","modelName":"name","remark":"名称","showInList":true,"showInForm":true,"formType":"singleLineText","required":true},{"name":"remark","modelName":"remark","remark":"备注","showInList":true,"showInForm":true,"formType":"singleLineText"}],"readTable":false,"dataSourceCode":"dataSource","genJson":"","subtables":[],"update":false,"basePackage":"com.izouma.nineth","tablePackage":"com.izouma.nineth.domain.Tag"}

+ 44 - 0
src/main/vue/src/components/TagSelect.vue

@@ -0,0 +1,44 @@
+<template>
+    <el-select v-model="selected" placeholder="请选择标签" multiple value-key="id">
+        <el-option v-for="item in tags" :key="item.id" :value="item" :label="item.name"></el-option>
+    </el-select>
+</template>
+<script>
+export default {
+    props: {
+        value: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data() {
+        return {
+            tags: [],
+            selected: []
+        };
+    },
+    computed: {},
+    created() {
+        if (this.value) {
+            this.selected = [...this.value];
+        }
+        this.$http.post('/tag/all', { size: 10000 }, { body: 'json' }).then(res => {
+            this.tags = res.content;
+        });
+    },
+    watch: {
+        value(val) {
+            if (val) {
+                if (val.map(tag => tag.id).join(',') !== this.selected.map(tag => tag.id).join(',')) {
+                    this.selected = [...val];
+                }
+            } else {
+                this.selected = [];
+            }
+        },
+        selected(val) {
+            this.$emit('input', [...val]);
+        }
+    }
+};
+</script>

+ 18 - 2
src/main/vue/src/router.js

@@ -707,7 +707,23 @@ const router = new Router({
                     meta: {
                         title: 'captcha'
                     }
-                }
+                },
+                {
+                    path: '/tagEdit',
+                    name: 'TagEdit',
+                    component: () => import(/* webpackChunkName: "tagEdit" */ '@/views/TagEdit.vue'),
+                    meta: {
+                       title: '标签编辑',
+                    },
+                },
+                {
+                    path: '/tagList',
+                    name: 'TagList',
+                    component: () => import(/* webpackChunkName: "tagList" */ '@/views/TagList.vue'),
+                    meta: {
+                       title: '标签',
+                    },
+               }
                 /**INSERT_LOCATION**/
             ]
         },
@@ -767,4 +783,4 @@ router.beforeEach((to, from, next) => {
     }
 });
 
-export default router;
+export default router;

+ 5 - 1
src/main/vue/src/views/CollectionEdit.vue

@@ -63,6 +63,9 @@
                                 ></el-option>
                             </el-select>
                         </el-form-item>
+                        <el-form-item prop="tags" label="标签">
+                            <tag-select v-model="formData.tags" style="width: 495px"></tag-select>
+                        </el-form-item>
                     </div>
                     <el-form-item prop="detail" label="详情" style="width: calc(100vw - 450px)">
                         <rich-text v-model="formData.detail"></rich-text>
@@ -364,9 +367,10 @@ import resolveUrl from 'resolve-url';
 import ModelUpload from '../components/ModelUpload.vue';
 import { format, parse, isBefore } from 'date-fns';
 import SingleUpload from '../components/SingleUpload.vue';
+import TagSelect from '../components/TagSelect.vue';
 export default {
     name: 'CollectionEdit',
-    components: { ModelUpload, SingleUpload },
+    components: { ModelUpload, SingleUpload, TagSelect },
     created() {
         Promise.all([
             new Promise((resolve, reject) => {

+ 110 - 0
src/main/vue/src/views/TagEdit.vue

@@ -0,0 +1,110 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                删除
+            </el-button>
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form :model="formData" :rules="rules" ref="form" label-width="52px" label-position="right"
+                         size="small"
+                         style="max-width: 500px;">
+                        <el-form-item prop="name" label="名称">
+                                    <el-input v-model="formData.name"></el-input>
+                        </el-form-item>
+                        <el-form-item prop="remark" label="备注">
+                                    <el-input v-model="formData.remark"></el-input>
+                        </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">
+                            删除
+                        </el-button>
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+    export default {
+        name: 'TagEdit',
+        created() {
+            if (this.$route.query.id) {
+                this.$http
+                    .get('tag/get/' + this.$route.query.id)
+                    .then(res => {
+                        this.formData = res;
+                    })
+                    .catch(e => {
+                        console.log(e);
+                        this.$message.error(e.error);
+                    });
+            }
+        },
+        data() {
+            return {
+                saving: false,
+                formData: {
+                },
+                rules: {
+                    name: [
+                        {
+                            required: true,
+                            message: '请输入名称',
+                            trigger: 'blur'
+                        },
+                    ],
+                },
+            }
+        },
+        methods: {
+            onSave() {
+                this.$refs.form.validate((valid) => {
+                    if (valid) {
+                        this.submit();
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            submit() {
+                let data = {...this.formData};
+
+                this.saving = true;
+                this.$http
+                    .post('/tag/save', data, {body: 'json'})
+                    .then(res => {
+                        this.saving = false;
+                        this.$message.success('成功');
+                        this.$router.go(-1);
+                    })
+                    .catch(e => {
+                        console.log(e);
+                        this.saving = false;
+                        this.$message.error(e.error);
+                    });
+            },
+            onDelete() {
+                this.$confirm('删除将无法恢复,确认要删除么?', '警告', {type: 'error'}).then(() => {
+                    return this.$http.post(`/tag/del/${this.formData.id}`)
+                }).then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                }).catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                })
+            },
+        }
+    }
+</script>
+<style lang="less" scoped></style>

+ 178 - 0
src/main/vue/src/views/TagList.vue

@@ -0,0 +1,178 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <el-button
+                @click="addRow"
+                type="primary"
+                icon="el-icon-plus"
+                :disabled="fetchingData || downloading"
+                class="filter-item"
+            >
+                新增
+            </el-button>
+            <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button>
+        </page-title>
+        <div class="filters-container">
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <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="remark" 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>
+                    <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'TagList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/tag/all',
+            downloading: false,
+            sortStr: 'name'
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        beforeGetData() {
+            return { search: this.search, query: { del: false } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/tagEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/tagEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/tag/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/tag/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 55 - 4
src/test/java/com/izouma/nineth/CommonTest.java

@@ -6,13 +6,13 @@ import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.alipay.api.AlipayApiException;
 import com.alipay.api.internal.util.AlipaySignature;
+import com.fasterxml.jackson.annotation.JsonView;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.github.kevinsawicki.http.HttpRequest;
 import com.google.common.base.Splitter;
 import com.izouma.nineth.config.Constants;
-import com.izouma.nineth.domain.BaseEntity;
-import com.izouma.nineth.domain.BlindBoxItem;
-import com.izouma.nineth.domain.SettleRecord;
-import com.izouma.nineth.domain.User;
+import com.izouma.nineth.domain.*;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.dto.SandPaySettle;
 import com.izouma.nineth.dto.UserWithdraw;
@@ -29,6 +29,9 @@ import io.ipfs.multihash.Multihash;
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 import lombok.SneakyThrows;
 import net.coobird.thumbnailator.Thumbnails;
 import org.apache.commons.codec.EncoderException;
@@ -648,4 +651,52 @@ public class CommonTest {
         PrivateKey privKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(bytePriv));
         System.out.println(privKey);
     }
+
+    public class Views {
+        public class Public {
+        }
+
+        public class Internal extends Public {
+        }
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public class Item {
+        @JsonView(Views.Public.class)
+        public int id;
+
+        @JsonView(Views.Public.class)
+        public String itemName;
+
+        @JsonView(Views.Internal.class)
+        public String ownerName;
+    }
+
+    @Test
+    public void testJsonView() throws JsonProcessingException {
+        Item item = new Item(2, "book", "John");
+
+        ObjectMapper mapper = new ObjectMapper();
+        String result = mapper
+                .writerWithView(Views.Internal.class)
+                .writeValueAsString(item);
+
+        System.out.println(result);
+    }
+
+    @Test
+    public void testSet() {
+        Set<Tag> set = new HashSet<>();
+        set.add(new Tag(1L, "tag1", "tag1"));
+        set.add(new Tag(1L, "tag1", "tag1"));
+        set.add(new Tag(1L, "tag1", "tag1"));
+        System.out.println(set);
+    }
+
+    @Test
+    public void decrypt() throws Exception {
+        System.out.println(AESEncryptUtil.decrypt("3BBC5669B95729554056D247338D3BAECA4483E429514227058BF3503F2D42A0"));
+    }
 }

+ 26 - 0
src/test/java/com/izouma/nineth/TestClassTest.java

@@ -0,0 +1,26 @@
+package com.izouma.nineth;
+
+
+import com.izouma.nineth.domain.TestClass;
+import com.izouma.nineth.repo.TestClassRepo;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class TestClassTest extends ApplicationTests {
+    @Autowired
+    private TestClassRepo testClassRepo;
+
+    @Test
+    public void testDynamicUpdate() {
+        TestClass a = new TestClass();
+        a.setName("name");
+        testClassRepo.save(a);
+
+        a.setColumnA("a");
+        testClassRepo.save(a);
+
+        a.setColumnA(null);
+        testClassRepo.save(a);
+    }
+
+}

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

@@ -14,6 +14,8 @@ public class CollectionRepoTest extends ApplicationTests {
     private CollectionRepo collectionRepo;
     @Autowired
     private AssetService   assetService;
+    @Autowired
+    private TagRepo        tagRepo;
 
     @Test
     public void updateCDN() {
@@ -28,4 +30,9 @@ public class CollectionRepoTest extends ApplicationTests {
                 list.get(0).get(4),
                 list.get(0).get(5));
     }
+
+    @Test
+    public void testTag() {
+
+    }
 }