Sfoglia il codice sorgente

Merge branch 'dev' of http://git.izouma.com/xiongzhu/9th into dev

panhui 4 anni fa
parent
commit
f322201f74

+ 19 - 0
src/main/java/com/izouma/nineth/config/SchedulingConfig.java

@@ -0,0 +1,19 @@
+package com.izouma.nineth.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+@Configuration
+public class SchedulingConfig {
+    @Bean
+    public TaskScheduler taskScheduler() {
+        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
+        // 定时任务执行线程池核心线程数
+        taskScheduler.setPoolSize(50);
+        taskScheduler.setRemoveOnCancelPolicy(true);
+        taskScheduler.setThreadNamePrefix("Scheduler-");
+        return taskScheduler;
+    }
+}

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

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

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

@@ -1,6 +1,7 @@
 package com.izouma.nineth.domain;
 package com.izouma.nineth.domain;
 
 
 import com.izouma.nineth.annotations.Searchable;
 import com.izouma.nineth.annotations.Searchable;
+import com.izouma.nineth.converter.FileObjectConverter;
 import com.izouma.nineth.converter.FileObjectListConverter;
 import com.izouma.nineth.converter.FileObjectListConverter;
 import com.izouma.nineth.converter.PrivilegeListConverter;
 import com.izouma.nineth.converter.PrivilegeListConverter;
 import com.izouma.nineth.converter.PropertyListConverter;
 import com.izouma.nineth.converter.PropertyListConverter;
@@ -37,6 +38,10 @@ public class Collection extends BaseEntity {
     @Convert(converter = FileObjectListConverter.class)
     @Convert(converter = FileObjectListConverter.class)
     private List<FileObject> pic;
     private List<FileObject> pic;
 
 
+    @Column(columnDefinition = "TEXT")
+    @Convert(converter = FileObjectConverter.class)
+    private FileObject model3d;
+
     @ApiModelProperty("铸造者")
     @ApiModelProperty("铸造者")
     @Searchable
     @Searchable
     private String minter;
     private String minter;

+ 66 - 0
src/main/java/com/izouma/nineth/utils/FileUtils.java

@@ -3,13 +3,18 @@ package com.izouma.nineth.utils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
 
 
 import java.io.*;
 import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Path;
 import java.nio.file.attribute.PosixFileAttributeView;
 import java.nio.file.attribute.PosixFileAttributeView;
 import java.nio.file.attribute.PosixFileAttributes;
 import java.nio.file.attribute.PosixFileAttributes;
 import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.PosixFilePermissions;
 import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
 
 
 public class FileUtils {
 public class FileUtils {
 
 
@@ -200,5 +205,66 @@ public class FileUtils {
 
 
     }
     }
 
 
+    public static void unzip(InputStream in, File destDir) throws IOException {
+        try {
+            unzip(in, destDir, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            unzip(in, destDir, Charset.forName("GB2312"));
+        }
+    }
+
+    public static void unzip(InputStream in, File destDir, Charset charset) throws IOException {
+        byte[] buffer = new byte[1024];
+        ZipInputStream zis = new ZipInputStream(in);
+        ZipEntry zipEntry = zis.getNextEntry();
+        while (zipEntry != null) {
+            File newFile = newFile(destDir, zipEntry);
+            if (zipEntry.isDirectory()) {
+                if (!newFile.isDirectory() && !newFile.mkdirs()) {
+                    throw new IOException("Failed to create directory " + newFile);
+                }
+            } else {
+                // fix for Windows-created archives
+                File parent = newFile.getParentFile();
+                if (!parent.isDirectory() && !parent.mkdirs()) {
+                    throw new IOException("Failed to create directory " + parent);
+                }
 
 
+                // write file content
+                FileOutputStream fos = new FileOutputStream(newFile);
+                int len;
+                while ((len = zis.read(buffer)) > 0) {
+                    fos.write(buffer, 0, len);
+                }
+                fos.close();
+            }
+            zipEntry = zis.getNextEntry();
+        }
+        zis.closeEntry();
+        zis.close();
+    }
+
+    public static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
+        File destFile = new File(destinationDir, zipEntry.getName());
+
+        String destDirPath = destinationDir.getCanonicalPath();
+        String destFilePath = destFile.getCanonicalPath();
+
+        if (!destFilePath.startsWith(destDirPath + File.separator)) {
+            throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
+        }
+
+        return destFile;
+    }
+
+    public static File findInDir(File dir, String ext) {
+        if (!(dir.exists() && dir.isDirectory())) return null;
+        for (File file : Optional.ofNullable(dir.listFiles()).orElse(new File[0])) {
+            String name = file.getName().toLowerCase();
+            if (name.endsWith(ext.toLowerCase()) && !file.isHidden()) {
+                return file;
+            }
+        }
+        return null;
+    }
 }
 }

+ 28 - 5
src/main/java/com/izouma/nineth/web/FileUploadController.java

@@ -10,10 +10,10 @@ import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.util.TempFile;
 import org.bytedeco.javacv.FFmpegFrameGrabber;
 import org.bytedeco.javacv.FFmpegFrameGrabber;
 import org.bytedeco.javacv.Frame;
 import org.bytedeco.javacv.Frame;
 import org.bytedeco.javacv.Java2DFrameConverter;
 import org.bytedeco.javacv.Java2DFrameConverter;
-import org.pngquant.PngQuant;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -26,10 +26,7 @@ import java.awt.image.BufferedImage;
 import java.io.*;
 import java.io.*;
 import java.net.URLConnection;
 import java.net.URLConnection;
 import java.text.SimpleDateFormat;
 import java.text.SimpleDateFormat;
-import java.util.Base64;
-import java.util.Date;
-import java.util.Objects;
-import java.util.Optional;
+import java.util.*;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 
 
 
 
@@ -182,4 +179,30 @@ public class FileUploadController {
 
 
         return new FileObject(file.getOriginalFilename(), url, thumbUrl, file.getContentType());
         return new FileObject(file.getOriginalFilename(), url, thumbUrl, file.getContentType());
     }
     }
+
+    @PostMapping("/3dModel")
+    public FileObject upload3dModel(@RequestParam("file") MultipartFile file) throws IOException {
+        if (!"zip".equalsIgnoreCase(FilenameUtils.getExtension(file.getOriginalFilename()))) {
+            throw new BusinessException("只能上传zip");
+        }
+        File destDir = TempFile.createTempDirectory(RandomStringUtils.randomAlphabetic(20));
+        com.izouma.nineth.utils.FileUtils.unzip(file.getInputStream(), destDir);
+        File fbxFile = com.izouma.nineth.utils.FileUtils.findInDir(destDir, ".fbx");
+        if (fbxFile == null) {
+            throw new BusinessException("找不到fbx文件");
+        }
+        File fbxDir = fbxFile.getParentFile();
+        String basePath = "fbx/"
+                + new SimpleDateFormat("yyyy-MM_dd-HH").format(new Date()) + "/"
+                + RandomStringUtils.randomAlphabetic(16);
+        List<String> urls = new ArrayList<>();
+        for (File listFile : fbxDir.listFiles()) {
+            if (!listFile.isHidden() && !listFile.isDirectory()) {
+                urls.add(storageService.uploadFromInputStream(new FileInputStream(listFile), basePath + "/" + listFile.getName()));
+            }
+        }
+        String fbxUrl = urls.stream().filter(s -> s.toLowerCase().endsWith(".fbx")).findAny()
+                .orElseThrow(new BusinessException("找不到fbx文件"));
+        return new FileObject(fbxFile.getName(), fbxUrl, null, "fbx");
+    }
 }
 }

+ 5 - 2
src/main/vue/src/components/FileUpload.vue

@@ -1,13 +1,14 @@
 <template>
 <template>
     <el-upload
     <el-upload
         class="file-upload"
         class="file-upload"
-        :action="uploadUrl"
+        :action="customUrl || uploadUrl"
         :on-success="onSuccess"
         :on-success="onSuccess"
         :headers="headers"
         :headers="headers"
         :file-list="fileList"
         :file-list="fileList"
         :limit="filesLimit"
         :limit="filesLimit"
         :on-exceed="onExceed"
         :on-exceed="onExceed"
         :on-preview="onPreview"
         :on-preview="onPreview"
+        :accept="accept || '*/*'"
         ref="upload"
         ref="upload"
     >
     >
         <el-button type="primary" size="mini" slot="trigger"> 点击上传 </el-button>
         <el-button type="primary" size="mini" slot="trigger"> 点击上传 </el-button>
@@ -62,7 +63,9 @@ export default {
         format: {
         format: {
             type: String,
             type: String,
             default: 'string'
             default: 'string'
-        }
+        },
+        customUrl: {},
+        accept: {}
     },
     },
     data() {
     data() {
         return {
         return {

+ 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>

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

@@ -29,6 +29,17 @@
                         ></object-upload>
                         ></object-upload>
                         <div class="tip">支持JPG、PNG、GIF、MP4,推荐长宽比1:1</div>
                         <div class="tip">支持JPG、PNG、GIF、MP4,推荐长宽比1:1</div>
                     </el-form-item>
                     </el-form-item>
+                    <el-form-item prop="model3d" label="3D模型">
+                        <model-upload
+                            :limit="1"
+                            v-model="formData.model3d"
+                            :customUrl="customUrl"
+                            accept="application/zip"
+                            format="json"
+                            single
+                        ></model-upload>
+                        <div class="tip">请将FBX文件与贴图打包成zip压缩包上传</div>
+                    </el-form-item>
                     <el-form-item prop="minterId" label="铸造者">
                     <el-form-item prop="minterId" label="铸造者">
                         <minter-select
                         <minter-select
                             v-model="formData.minterId"
                             v-model="formData.minterId"
@@ -221,8 +232,11 @@
     </div>
     </div>
 </template>
 </template>
 <script>
 <script>
+import resolveUrl from 'resolve-url';
+import ModelUpload from '../components/ModelUpload.vue';
 export default {
 export default {
     name: 'CollectionEdit',
     name: 'CollectionEdit',
+    components: { ModelUpload },
     created() {
     created() {
         Promise.all([
         Promise.all([
             new Promise((resolve, reject) => {
             new Promise((resolve, reject) => {
@@ -418,7 +432,8 @@ export default {
             privelegeRules: {
             privelegeRules: {
                 detail: [{ required: true, message: '请填写内容' }],
                 detail: [{ required: true, message: '请填写内容' }],
                 remark: [{ required: true, message: '请填写说明' }]
                 remark: [{ required: true, message: '请填写说明' }]
-            }
+            },
+            customUrl: resolveUrl(this.$baseUrl, 'upload/3dModel')
         };
         };
     },
     },
     methods: {
     methods: {

+ 3 - 14
src/test/java/com/izouma/nineth/service/AssetServiceTest.java

@@ -83,20 +83,9 @@ class AssetServiceTest extends ApplicationTests {
     @Test
     @Test
     public void stat() {
     public void stat() {
         Map<User, Integer> match = assetService.holdQuery(Arrays.asList(
         Map<User, Integer> match = assetService.holdQuery(Arrays.asList(
-                        "《第九空间》- 摩羯座权益卡",
-                        "《第九空间》- 射手座权益卡",
-                        "《第九空间》- 天蝎座权益卡",
-                        "《第九空间》- 天秤座权益卡",
-                        "《第九空间》- 处女座权益卡",
-                        "《第九空间》- 狮子座权益卡",
-                        "《第九空间》- 巨蟹座权益卡",
-                        "《第九空间》- 双子座权益卡",
-                        "《第九空间》- 金牛座权益卡",
-                        "《第九空间》- 白羊座权益卡",
-                        "《第九空间》- 双鱼座权益卡",
-                        "《第九空间》- 水瓶座权益卡"),
-                LocalDateTime.of(2021, 12, 24, 0, 0, 0),
-                LocalDateTime.of(2021, 12, 24, 23, 59, 59));
+                       "【熊猫柯斯】-虎虎生威"),
+                LocalDateTime.of(2021, 12, 31, 9, 59, 59),
+                LocalDateTime.of(2021, 12, 31, 10, 0, 0));
         System.out.println("用户ID,昵称,手机,持有套数");
         System.out.println("用户ID,昵称,手机,持有套数");
         for (Map.Entry<User, Integer> e : match.entrySet()) {
         for (Map.Entry<User, Integer> e : match.entrySet()) {
             User user = e.getKey();
             User user = e.getKey();