Jelajahi Sumber

Merge branch 'dev'

xiongzhu 4 tahun lalu
induk
melakukan
8d2f08b5ba

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

@@ -1,6 +1,7 @@
 package com.izouma.nineth.domain;
 
 import com.izouma.nineth.annotations.Searchable;
+import com.izouma.nineth.converter.FileObjectConverter;
 import com.izouma.nineth.converter.FileObjectListConverter;
 import com.izouma.nineth.converter.PrivilegeListConverter;
 import com.izouma.nineth.converter.PropertyListConverter;
@@ -96,6 +97,10 @@ public class Asset extends BaseEntity {
     @Column(columnDefinition = "TEXT")
     private List<FileObject> pic;
 
+    @Column(columnDefinition = "TEXT")
+    @Convert(converter = FileObjectConverter.class)
+    private FileObject model3d;
+
     @ApiModelProperty("tokenId")
     private String tokenId;
 
@@ -154,6 +159,7 @@ public class Asset extends BaseEntity {
                 .name(collection.getName())
                 .detail(collection.getDetail())
                 .pic(collection.getPic())
+                .model3d(collection.getModel3d())
                 .properties(collection.getProperties())
                 .privileges(collection.getPrivileges())
                 .category(collection.getCategory())
@@ -178,6 +184,7 @@ public class Asset extends BaseEntity {
                 .name(item.getName())
                 .detail(item.getDetail())
                 .pic(item.getPic())
+                .model3d(item.getModel3d())
                 .properties(item.getProperties())
                 .privileges(item.getPrivileges())
                 .category(item.getCategory())

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

@@ -36,6 +36,10 @@ public class BlindBoxItem extends BaseEntity {
     @Convert(converter = FileObjectListConverter.class)
     private List<FileObject> pic;
 
+    @Column(columnDefinition = "TEXT")
+    @Convert(converter = FileObjectListConverter.class)
+    private FileObject model3d;
+
     @ApiModelProperty("铸造者")
     @Searchable
     private String minter;

+ 4 - 1
src/main/java/com/izouma/nineth/domain/Collection.java

@@ -1,6 +1,7 @@
 package com.izouma.nineth.domain;
 
 import com.izouma.nineth.annotations.Searchable;
+import com.izouma.nineth.converter.FileObjectConverter;
 import com.izouma.nineth.converter.FileObjectListConverter;
 import com.izouma.nineth.converter.PrivilegeListConverter;
 import com.izouma.nineth.converter.PropertyListConverter;
@@ -38,7 +39,7 @@ public class Collection extends BaseEntity {
     private List<FileObject> pic;
 
     @Column(columnDefinition = "TEXT")
-    @Convert(converter = FileObjectListConverter.class)
+    @Convert(converter = FileObjectConverter.class)
     private FileObject model3d;
 
     @ApiModelProperty("铸造者")
@@ -137,4 +138,6 @@ public class Collection extends BaseEntity {
     private int soldOut;
 
     private int maxCount;
+
+    private boolean scanCode;
 }

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

@@ -49,4 +49,7 @@ public interface AssetRepo extends JpaRepository<Asset, Long>, JpaSpecificationE
 
 
     List<Asset> findByTokenIdAndCreatedAtBetween(String tokenId, LocalDateTime start, LocalDateTime end);
+
+    Asset findFirstByTokenId(String tokenId);
+
 }

+ 16 - 3
src/main/java/com/izouma/nineth/repo/CollectionRepo.java

@@ -1,6 +1,9 @@
 package com.izouma.nineth.repo;
 
 import com.izouma.nineth.domain.Collection;
+import com.izouma.nineth.domain.CollectionProperty;
+import com.izouma.nineth.domain.FileObject;
+import com.izouma.nineth.domain.Privilege;
 import com.izouma.nineth.dto.RecommendCollection;
 import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.cache.annotation.CachePut;
@@ -23,8 +26,18 @@ public interface CollectionRepo extends JpaRepository<Collection, Long>, JpaSpec
     @CacheEvict(value = {"collection", "recommend"}, allEntries = true)
     void softDelete(Long id);
 
+    @Transactional
+    @Modifying
+    @Query("update Collection c set c.onShelf = ?2, c.salable = ?3, c.startTime = ?4, " +
+            "c.scheduleSale = ?5, c.sort = ?6, c.detail = ?7, c.privileges = ?8, " +
+            "c.properties = ?9, c.model3d = ?10 where c.id = ?1")
+    @CacheEvict(value = {"collection", "recommend"}, allEntries = true)
+    void update(@Nonnull Long id, boolean onShelf, boolean salable, LocalDateTime startTime,
+                boolean schedule, int sort, String detail, List<Privilege> privileges,
+                List<CollectionProperty> properties, FileObject model3d);
+
     @Cacheable("collection")
-    Optional<Collection> findById(Long id);
+    Optional<Collection> findById(@Nonnull Long id);
 
     Optional<Collection> findByIdAndDelFalse(Long id);
 
@@ -80,9 +93,9 @@ public interface CollectionRepo extends JpaRepository<Collection, Long>, JpaSpec
 
     @Transactional
     @Modifying
-    @Query("update Collection c set c.onShelf = ?2, c.salable = ?2 where c.id = ?1")
+    @Query("update Collection c set c.onShelf = true, c.salable = true where c.id = ?1")
     @CacheEvict(value = "collection", key = "#id")
-    void scheduleOnShelf(Long id, boolean onShelf);
+    void scheduleOnShelf(Long id);
 
     @Query("select c.currentNumber from Collection c where c.id = ?1")
     Optional<Integer> getCurrentNumber(Long id);

+ 23 - 0
src/main/java/com/izouma/nineth/service/CacheService.java

@@ -0,0 +1,23 @@
+package com.izouma.nineth.service;
+
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CacheService {
+    @CacheEvict(value = "collection", allEntries = true)
+    public void clearCollection() {
+    }
+
+    @CacheEvict(value = "collection", key = "#id")
+    public void clearCollection(Long id) {
+    }
+
+    @CacheEvict(value = "user", allEntries = true)
+    public void clearUser() {
+    }
+
+    @CacheEvict(value = "user", key = "#username")
+    public void clearUser(String username) {
+    }
+}

+ 46 - 17
src/main/java/com/izouma/nineth/service/CollectionService.java

@@ -1,7 +1,7 @@
 package com.izouma.nineth.service;
 
-import com.izouma.nineth.domain.*;
 import com.izouma.nineth.domain.Collection;
+import com.izouma.nineth.domain.*;
 import com.izouma.nineth.dto.CollectionDTO;
 import com.izouma.nineth.dto.CreateBlindBox;
 import com.izouma.nineth.dto.PageQuery;
@@ -17,13 +17,11 @@ import org.apache.commons.lang3.RandomUtils;
 import org.apache.commons.lang3.Range;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
-import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.domain.Specification;
-import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.TaskScheduler;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
@@ -32,28 +30,32 @@ import javax.annotation.PostConstruct;
 import javax.persistence.criteria.Predicate;
 import javax.transaction.Transactional;
 import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.*;
+import java.util.concurrent.ScheduledFuture;
 import java.util.stream.Collectors;
 
 @Service
 @AllArgsConstructor
 public class CollectionService {
 
-    private CollectionRepo                collectionRepo;
-    private LikeRepo                      likeRepo;
-    private BlindBoxItemRepo              blindBoxItemRepo;
-    private AppointmentRepo               appointmentRepo;
-    private UserRepo                      userRepo;
-    private AssetService                  assetService;
-    private RedisTemplate<String, Object> redisTemplate;
-    private TaskScheduler                 taskScheduler;
+    private CollectionRepo   collectionRepo;
+    private LikeRepo         likeRepo;
+    private BlindBoxItemRepo blindBoxItemRepo;
+    private AppointmentRepo  appointmentRepo;
+    private UserRepo         userRepo;
+    private AssetService     assetService;
+    private TaskScheduler    taskScheduler;
+    private CacheService     cacheService;
+
+    private final Map<Long, ScheduledFuture<?>> tasks = new HashMap<>();
 
     @PostConstruct
     public void init() {
-    }
-
-    @CacheEvict(value = "collection", allEntries = true)
-    public void clearCache() {
+        List<Collection> collections = collectionRepo.findByScheduleSaleTrueAndOnShelfFalseAndStartTimeBeforeAndDelFalse(LocalDateTime.now());
+        for (Collection collection : collections) {
+            onShelfTask(collection);
+        }
     }
 
     public Page<Collection> all(PageQuery pageQuery) {
@@ -105,7 +107,34 @@ public class CollectionService {
             }
             record.setOnShelf(record.getStartTime().isBefore(LocalDateTime.now()));
         }
-        return collectionRepo.save(record);
+        collectionRepo.save(record);
+        if (record.isScheduleSale()) {
+            onShelfTask(record);
+        }
+        return record;
+    }
+
+    private void onShelfTask(Collection record) {
+        if (record.getStartTime().minusSeconds(2).isAfter(LocalDateTime.now())) {
+            Date date = Date.from(record.getStartTime().atZone(ZoneId.systemDefault()).toInstant());
+            ScheduledFuture<?> future = taskScheduler.schedule(() -> {
+                collectionRepo.scheduleOnShelf(record.getId());
+                tasks.remove(record.getId());
+            }, date);
+            tasks.put(record.getId(), future);
+        } else {
+            collectionRepo.scheduleOnShelf(record.getId());
+        }
+    }
+
+    public Collection update(Collection record) {
+        Collection orig = collectionRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+        collectionRepo.update(record.getId(), record.isOnShelf(), record.isSalable(),
+                record.getStartTime(), record.isScheduleSale(), record.getSort(),
+                record.getDetail(), record.getPrivileges(), record.getProperties(),
+                record.getModel3d());
+
+        return collectionRepo.save(orig);
     }
 
     public CollectionDTO toDTO(Collection collection) {
@@ -220,7 +249,7 @@ public class CollectionService {
     public void scheduleOnShelf() {
         List<Collection> collections = collectionRepo.findByScheduleSaleTrueAndOnShelfFalseAndStartTimeBeforeAndDelFalse(LocalDateTime.now());
         for (Collection collection : collections) {
-            collectionRepo.scheduleOnShelf(collection.getId(), true);
+            collectionRepo.scheduleOnShelf(collection.getId());
         }
     }
 

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

@@ -102,7 +102,9 @@ public class OrderService {
             }
         }
         if (!collection.isOnShelf()) {
-            throw new BusinessException("藏品已下架");
+            if (!collection.isScanCode()) {
+                throw new BusinessException("藏品已下架");
+            }
         }
         if (qty > collection.getStock()) {
             throw new BusinessException("库存不足");

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

@@ -65,6 +65,7 @@ public class UserService {
     private UserBankCardRepo  userBankCardRepo;
     private InviteRepo        inviteRepo;
     private NFTService        nftService;
+    private CacheService      cacheService;
 
     @CacheEvict(value = "user", key = "#user.username")
     public User update(User user) {
@@ -78,14 +79,10 @@ public class UserService {
         userRepo.updateOrderMinter(orig.getId());
         userRepo.updateHistoryFromUser(orig.getId());
         userRepo.updateHistoryToUser(orig.getId());
-        collectionService.clearCache();
+        cacheService.clearCollection();
         return orig;
     }
 
-    @CacheEvict(value = "user", allEntries = true)
-    public void clearCache() {
-    }
-
     public Page<User> all(PageQuery pageQuery) {
         Specification<User> specification = JpaUtils.toSpecification(pageQuery, User.class);
 

+ 243 - 0
src/main/vue/src/components/ModelUpload.vue

@@ -0,0 +1,243 @@
+<template>
+    <el-upload
+        class="file-upload"
+        :action="customUrl || uploadUrl"
+        :on-success="onSuccess"
+        :headers="headers"
+        :file-list="fileList"
+        :limit="filesLimit"
+        :on-exceed="onExceed"
+        :on-preview="onPreview"
+        :accept="accept || '*/*'"
+        ref="upload"
+    >
+        <el-button type="primary" size="mini" slot="trigger"> 点击上传 </el-button>
+        <div class="file-list-item" slot="file" slot-scope="{ file }">
+            <div class="file-name">
+                <i class="status-icon el-icon-warning-outline danger" v-if="file.status === 'fail'"></i>
+                <i class="status-icon el-icon-circle-check success" v-else-if="file.status === 'success'"></i>
+                <i class="status-icon el-icon-loading" v-else></i>
+                {{ file.name }}
+                <i class="opt">
+                    <i
+                        class="opt-icon el-icon-search"
+                        v-if="file.status === 'success' && isImage(file)"
+                        @click="preview(file)"
+                    ></i>
+                    <i class="opt-icon el-icon-download" v-if="file.status === 'success'" @click="download(file)"></i>
+                    <i class="opt-icon el-icon-delete" @click="removeFile(file)"></i>
+                </i>
+            </div>
+            <el-progress
+                v-if="file.status === 'uploading'"
+                :percentage="file.percentage"
+                :show-text="false"
+                :stroke-width="2"
+                class="upload-progress"
+            ></el-progress>
+        </div>
+        <el-image style="width: 0; height: 0" :src="previewUrl" :preview-src-list="[previewUrl]" ref="preview">
+        </el-image>
+    </el-upload>
+</template>
+
+<script>
+import resolveUrl from 'resolve-url';
+import axios from 'axios';
+export default {
+    name: 'FileUpload',
+    props: {
+        single: {
+            type: Boolean,
+            default() {
+                return false;
+            }
+        },
+        limit: {
+            type: Number,
+            default() {
+                return 10000;
+            }
+        },
+        value: {},
+        format: {
+            type: String,
+            default: 'string'
+        },
+        customUrl: {},
+        accept: {}
+    },
+    data() {
+        return {
+            fileList: [],
+            emitting: false,
+            uploadUrl: '',
+            previewUrl: null
+        };
+    },
+    computed: {
+        headers() {
+            return {
+                Authorization: 'Bearer ' + localStorage.getItem('token')
+            };
+        },
+        filesLimit() {
+            if (this.single) {
+                return 1;
+            }
+            return this.limit;
+        },
+        disabled() {
+            return this.fileList.length >= this.limit;
+        }
+    },
+    created() {
+        this.uploadUrl = resolveUrl(this.$baseUrl, 'upload/file');
+        this.update(this.value);
+    },
+    methods: {
+        onSuccess(res, file, fileList) {
+            this.fileList = [res];
+            this.emit();
+        },
+        update(value) {
+            if (this.filesLimit === 1) {
+                if (this.format === 'json') {
+                    this.fileList = value ? [{ name: value.name, url: value.url }] : [];
+                } else {
+                    this.fileList = value ? [{ name: value.split('/').pop(), url: value }] : [];
+                }
+            } else {
+                if (!value) {
+                    this.fileList = [];
+                } else {
+                    this.fileList = value.map(i => {
+                        return { name: i.name, url: i.url };
+                    });
+                }
+            }
+        },
+        onExceed(files, fileList) {
+            console.log(files, fileList);
+            this.$message.error(`最多上传${this.filesLimit}个文件`);
+        },
+        onPreview(file) {
+            console.log(file);
+        },
+        removeFile(file) {
+            if (file.status === 'uploading') {
+                this.$refs.upload.abort(file);
+            } else if (file.status === 'success') {
+                let index = this.fileList.findIndex(i => i.url === file.url);
+                if (index > -1) {
+                    this.fileList.splice(index, 1);
+                }
+            }
+            this.emit();
+        },
+        download(file) {
+            window.open(file.url, '_blank');
+        },
+        preview(file) {
+            this.previewUrl = file.url;
+            this.$nextTick(() => {
+                this.$refs.preview.clickHandler();
+            });
+        },
+        isImage(file) {
+            return /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.url);
+        },
+        emit() {
+            this.emitting = true;
+            if (this.filesLimit === 1) {
+                if (this.format === 'json') {
+                    this.$emit(
+                        'input',
+                        this.fileList[0]
+                            ? {
+                                  name: this.fileList[0].name,
+                                  url: this.fileList[0].url
+                              }
+                            : null
+                    );
+                } else {
+                    this.$emit('input', this.fileList[0] ? this.fileList[0].url : null);
+                }
+            } else {
+                if (this.format === 'json') {
+                    this.$emit(
+                        'input',
+                        this.fileList.map(i => {
+                            return {
+                                name: i.name,
+                                url: i.url
+                            };
+                        })
+                    );
+                } else {
+                    this.$emit(
+                        'input',
+                        this.fileList.map(i => i.url)
+                    );
+                }
+            }
+            this.$nextTick(() => {
+                this.emitting = false;
+            });
+        }
+    },
+    watch: {
+        value(value) {
+            if (this.emitting) return;
+            this.update(value);
+        }
+    }
+};
+</script>
+
+<style lang="less" scoped>
+.file-list-item {
+    line-height: 1.8;
+    margin-top: 5px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+    cursor: pointer;
+    .file-name {
+        padding: 0 90px 0 20px;
+    }
+    .upload-progress {
+        margin-top: 2px;
+        position: absolute;
+        bottom: 0;
+        left: 20px;
+        right: 0;
+        width: auto;
+    }
+    .danger {
+        color: #f56c6c;
+    }
+    .success {
+        color: @success;
+    }
+    .status-icon {
+        position: absolute;
+        left: 0;
+        top: 0;
+        line-height: inherit;
+    }
+    .opt {
+        position: absolute;
+        right: 0;
+        top: 0;
+        line-height: inherit;
+        .opt-icon {
+            margin-left: 15px;
+            transition: color 0.3s;
+            &:hover {
+                color: @prim;
+            }
+        }
+    }
+}
+</style>

+ 6 - 2
src/main/vue/src/views/CollectionEdit.vue

@@ -30,12 +30,14 @@
                         <div class="tip">支持JPG、PNG、GIF、MP4,推荐长宽比1:1</div>
                     </el-form-item>
                     <el-form-item prop="model3d" label="3D模型">
-                        <file-upload
+                        <model-upload
                             :limit="1"
                             v-model="formData.model3d"
                             :customUrl="customUrl"
                             accept="application/zip"
-                        ></file-upload>
+                            format="json"
+                            single
+                        ></model-upload>
                         <div class="tip">请将FBX文件与贴图打包成zip压缩包上传</div>
                     </el-form-item>
                     <el-form-item prop="minterId" label="铸造者">
@@ -231,8 +233,10 @@
 </template>
 <script>
 import resolveUrl from 'resolve-url';
+import ModelUpload from '../components/ModelUpload.vue';
 export default {
     name: 'CollectionEdit',
+    components: { ModelUpload },
     created() {
         Promise.all([
             new Promise((resolve, reject) => {

+ 13 - 0
src/test/java/com/izouma/nineth/CommonTest.java

@@ -18,6 +18,7 @@ import lombok.SneakyThrows;
 import net.coobird.thumbnailator.Thumbnails;
 import org.apache.commons.codec.EncoderException;
 import org.apache.commons.codec.net.URLCodec;
+import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.RandomUtils;
 import org.apache.commons.lang3.Range;
@@ -381,4 +382,16 @@ public class CommonTest {
         System.out.println(tokenId);
         System.out.println(new BigInteger(tokenId, 16));
     }
+
+    @Test
+    public void dd() throws IOException {
+        String[] arr1 = FileUtils.readFileToString(new File("/Users/drew/Desktop/未命名文件夹 2/111")).split("\n");
+        List<String> arr2 = Arrays.asList(FileUtils.readFileToString(new File("/Users/drew/Desktop/未命名文件夹 2/222"))
+                .split("\n"));
+        for (String s : arr1) {
+            if (!arr2.contains(s)) {
+                System.out.println(s);
+            }
+        }
+    }
 }

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

@@ -129,4 +129,39 @@ class AssetServiceTest extends ApplicationTests {
         });
         System.out.println(builder);
     }
+
+    @Test
+    public void del() {
+        for (String s : new String[]{"5df454549a0f4373ce6bb81fe7bbb941",
+                "61773ab99f4a402cbe2a667d53fdc11c",
+                "69d43dda0dfdf08dd9ecea3ad1f9d50",
+                "6de0578f99af8ae285b6eefd7e834f22",
+                "7433fedd607cd9df192e4303206ebbf4",
+                "7e4e23b9eebf1ab16d820da5c5eb3064",
+                "86d9437a6551174bf7b8d4c46ba01690",
+                "8e16830b0a36d661b90d5b1d8723f0d5",
+                "953862a50f982d0495edf294c2c0c8c0",
+                "a1da155eb43ef398ea30b4dffad8e184",
+                "b74c8d9d59df3675e26d846892e2dad4",
+                "bc47260b0ef9c3fe2616b80c31385677",
+                "c50f01943fd38ebadb26fb68877477ae",
+                "d0667e1585327bd42c75fd98ca414487",
+                "d646bd4e0357fbe067ab8d0504ed57c4",
+                "e0ddee55cfe14fbc18770631b10386bb",
+                "e398353b6838b6b9ce9c903b448eaeb3",
+                "ee35149d07d34e39adb18dfb1a0c7021",
+                "faebc681f3698bf0da6136d0c8be87ac",
+                "fc38421ee6d402a24e5c25ba2d870831",
+                "43800381853ce6e07bec0fd602a54a69",
+                "48918cf5bd017164ea5d1927708e8b9c",
+                "493f285ae429aa4b1760db3e37e2a76f",
+                "49bfe48771acf39e084e331be79e762d",
+                "52ba65e1416fde9c3e9528e34b587454"
+        }) {
+            Asset asset = assetRepo.findFirstByTokenId(s);
+            if (asset != null) {
+                assetRepo.delete(asset);
+            }
+        }
+    }
 }

+ 0 - 1
src/test/java/com/izouma/nineth/service/UserServiceTest.java

@@ -29,7 +29,6 @@ public class UserServiceTest extends ApplicationTests {
 
     @Test
     public void findByUsernameAndDelFalse1() {
-        userService.clearCache();
         userRepo.findByUsernameAndDelFalse("admin");
         userRepo.findByUsernameAndDelFalse("admin");
     }