Преглед изворни кода

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

xuqiang пре 4 година
родитељ
комит
e0505daa2a

BIN
lib/pngquant4j-1.0.jar


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

@@ -4,6 +4,7 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.retry.annotation.EnableRetry;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import springfox.documentation.swagger2.annotations.EnableSwagger2;
@@ -14,6 +15,7 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2;
 @EnableCaching
 @EnableScheduling
 @EnableAsync
+@EnableRetry
 public class Application {
 
     public static void main(String[] args) {

+ 2 - 0
src/main/java/com/izouma/nineth/dto/UserRegister.java

@@ -34,4 +34,6 @@ public class UserRegister {
     private String phone;
 
     private String email;
+
+    private boolean admin;
 }

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

@@ -10,9 +10,7 @@ import com.izouma.nineth.enums.CollectionSource;
 import com.izouma.nineth.enums.CollectionType;
 import com.izouma.nineth.event.CreateAssetEvent;
 import com.izouma.nineth.exception.BusinessException;
-import com.izouma.nineth.repo.AssetRepo;
-import com.izouma.nineth.repo.CollectionRepo;
-import com.izouma.nineth.repo.UserRepo;
+import com.izouma.nineth.repo.*;
 import com.izouma.nineth.utils.JpaUtils;
 import io.ipfs.api.IPFS;
 import io.ipfs.api.MerkleNode;
@@ -23,6 +21,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.context.ApplicationContext;
 import org.springframework.data.domain.Page;
+import org.springframework.retry.annotation.Backoff;
+import org.springframework.retry.annotation.Retryable;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 

+ 3 - 0
src/main/java/com/izouma/nineth/service/NFTService.java

@@ -19,6 +19,8 @@ import com.izouma.nineth.utils.HashUtils;
 import com.izouma.nineth.utils.SnowflakeIdWorker;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.retry.annotation.Backoff;
+import org.springframework.retry.annotation.Retryable;
 import org.springframework.stereotype.Service;
 
 import java.math.BigInteger;
@@ -56,6 +58,7 @@ public class NFTService {
         }
     }
 
+    @Retryable(maxAttempts = 10, backoff = @Backoff(delay = 5000), value = BusinessException.class)
     public NFT createToken(String toAccount) throws Exception {
         JSONArray jsonArray = new JSONArray();
         jsonArray.add(Utils.getIdentityByName(toAccount));

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

@@ -299,6 +299,15 @@ public class OrderService {
         }
     }
 
+    public void createAsset(Long orderId, Long itemId) {
+        assetService.createAsset(orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在")),
+                blindBoxItemRepo.findById(itemId).orElseThrow(new BusinessException("item不存在")));
+    }
+
+    public void createAsset(Long orderId) {
+        assetService.createAsset(orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在")));
+    }
+
     @EventListener
     public void onCreateAsset(CreateAssetEvent event) {
         Order order = event.getOrder();

+ 12 - 1
src/main/java/com/izouma/nineth/utils/ImageUtils.java

@@ -30,8 +30,19 @@ public class ImageUtils {
         return img;
     }
 
+    public static BufferedImage resizePng1(InputStream image, int width, int height, boolean compress) throws IOException {
+        BufferedImage bufferedImage = ImageIO.read(image);
+        BufferedImage img = Thumbnails.of(bufferedImage)
+                .size(Math.min(width, bufferedImage.getWidth()), Math.min(height, bufferedImage.getHeight()))
+                .outputFormat("png")
+                .asBufferedImage();
+        bufferedImage.flush();
+        PngQuant pngQuant = new PngQuant();
+        return pngQuant.getRemapped(img);
+    }
+
     public static void main(String[] args) throws IOException {
-        ImageUtils.resizePng(new FileInputStream("/Users/drew/Downloads/盲盒详情.png"), 1000, 2000,true);
+        ImageUtils.resizePng(new FileInputStream("/Users/drew/Downloads/盲盒详情.png"), 1000, 2000, true);
         PngQuant pngQuant = new PngQuant();
         ImageIO.write(pngQuant
                         .getRemapped(ImageIO.read(new File("/Users/drew/Desktop/1.png"))),

+ 59 - 4
src/main/java/com/izouma/nineth/web/FileUploadController.java

@@ -97,6 +97,62 @@ public class FileUploadController {
         return storageService.uploadFromInputStream(file.getInputStream(), path);
     }
 
+    @PostMapping("/file1")
+    public String uploadFile1(@RequestParam("file") MultipartFile file,
+                             @RequestParam(value = "path", required = false) String path,
+                             @RequestParam(value = "compress", defaultValue = "false") boolean compress,
+                             @RequestParam(value = "width", required = false) Integer width,
+                             @RequestParam(value = "height", required = false) Integer height) throws IOException {
+
+        String ext = Optional.ofNullable(FilenameUtils.getExtension(file.getOriginalFilename())).orElse("")
+                .toLowerCase().replace("jpeg", "jpg");
+        if (path == null) {
+            String basePath = Optional.ofNullable(file.getContentType()).orElse("application").split("/")[0];
+            path = basePath + "/" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date())
+                    + RandomStringUtils.randomAlphabetic(8)
+                    + "." + ext;
+        }
+
+        if (Pattern.matches("(jpg|png)", ext) && compress) {
+            if (width == null && height == null) {
+                width = Integer.MAX_VALUE;
+                height = Integer.MAX_VALUE;
+            } else if (height == null) {
+                height = Integer.MAX_VALUE;
+            } else if (width == null) {
+                width = Integer.MAX_VALUE;
+            }
+            BufferedImage img = null;
+            if ("jpg".equals(ext)) {
+                img = ImageUtils.resizeJpg(file.getInputStream(), width, height);
+            } else if ("png".equals(ext)) {
+                img = ImageUtils.resizePng1(file.getInputStream(), width, height, true);
+            }
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            ImageIO.write(img, ext, os);
+            InputStream is = new ByteArrayInputStream(os.toByteArray());
+            return storageService.uploadFromInputStream(is, path);
+        } else if (width != null || height != null) {
+            if (height == null) {
+                height = Integer.MAX_VALUE;
+            } else if (width == null) {
+                width = Integer.MAX_VALUE;
+            }
+            BufferedImage img = null;
+            if ("jpg".equals(ext)) {
+                img = ImageUtils.resizeJpg(file.getInputStream(), width, height);
+            } else if ("png".equals(ext)) {
+                img = ImageUtils.resizePng1(file.getInputStream(), width, height, false);
+            }
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            ImageIO.write(img, ext, os);
+            InputStream is = new ByteArrayInputStream(os.toByteArray());
+            return storageService.uploadFromInputStream(is, path);
+        }
+
+        return storageService.uploadFromInputStream(file.getInputStream(), path);
+    }
+
     @PostMapping("/base64")
     public String uploadImage(@RequestParam("base64") String base64,
                               @RequestParam(value = "path", required = false) String path) {
@@ -196,15 +252,14 @@ public class FileUploadController {
                 bi = aa.convert(f);
                 File thumbFile = null;
                 while (bi != null) {
-                    thumbFile = File.createTempFile("video_thumb_", ".png");
-                    PngQuant pngQuant = new PngQuant();
-                    ImageIO.write(pngQuant.getRemapped(bi), "png", thumbFile);
+                    thumbFile = File.createTempFile("video_thumb_", ".jpg");
+                    ImageIO.write(bi, "jpg", thumbFile);
                     f = frameGrabber.grabKeyFrame();
                     bi = aa.convert(f);
                 }
                 Objects.requireNonNull(thumbFile);
                 String thumbPath = "thumb_image/" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date())
-                        + RandomStringUtils.randomAlphabetic(8) + ".png";
+                        + RandomStringUtils.randomAlphabetic(8) + ".jpg";
                 thumbUrl = storageService.uploadFromInputStream(new FileInputStream(thumbFile), thumbPath);
             } catch (Exception e) {
                 e.printStackTrace();

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

@@ -77,5 +77,14 @@ public class OrderController extends BaseController {
         return orderService.create(SecurityUtils.getAuthenticatedUser().getId(), collectionId, qty, addressId);
     }
 
+    @GetMapping("/createAsset")
+    public void createAsset(@RequestParam Long orderId, @RequestParam(required = false) Long itemId) {
+        if (itemId == null) {
+            orderService.createAsset(orderId);
+        } else {
+            orderService.createAsset(orderId, itemId);
+        }
+
+    }
 }
 

+ 1 - 1
src/main/nine-space/src/views/Discover.vue

@@ -122,7 +122,7 @@ export default {
                         query: {
                             type: 'DISCOVER'
                         },
-                        sort: 'createdAt,desc'
+                        sort: 'sort,asc;createdAt,desc'
                     },
                     { body: 'json' }
                 )

+ 1 - 1
src/main/nine-space/src/views/Home.vue

@@ -121,7 +121,7 @@ export default {
                         query: {
                             type: 'HOME'
                         },
-                        sort: 'createdAt,desc'
+                        sort: 'sort,asc;createdAt,desc'
                     },
                     { body: 'json' }
                 )

+ 1 - 1
src/main/nine-space/src/views/Index.vue

@@ -31,7 +31,7 @@ export default {
             },
             {
                 name: 'store',
-                title: '柜子',
+                title: '藏品室',
                 icon: require('../assets/tabbar_icon_03.png'),
                 preIcon: require('../assets/tabbar_icon_03_pre.png')
             },

BIN
src/main/pc-space/src/assets/defaultBg.jpg


+ 225 - 0
src/main/pc-space/src/components/AssetInfo.vue

@@ -0,0 +1,225 @@
+<template>
+    <router-link
+        :to="{
+            path: '/assetDetail',
+            query: {
+                id: info.id
+            }
+        }"
+        class="collect"
+    >
+        <el-image class="imgBox" :src="getImg(changeImgs(info.pic))" fit="cover"></el-image>
+
+        <div class="introduce">{{ info.name }}</div>
+        <div class="price" v-if="info.status === 'ON_SALE'">
+            <img class="img1" src="../assets/img/icon_jiage@3x.png" alt="" />
+            <div class="num">{{ info.price }}</div>
+        </div>
+        <div class="status" v-else-if="info.status === 'NORMAL'">
+            {{ info.publicShow ? '仅展示' : '未展示' }}
+        </div>
+        <div class="status" v-else>
+            {{ getLabelName(info.status, assetStatusOptions) }}
+        </div>
+        <div class="border"></div>
+        <div class="fans">
+            <div class="text">
+                <div class="text1 name1">
+                    <img class="text2" :src="info.minterAvatar" alt="" />
+                    <div class="text3">{{ info.minter }}</div>
+                </div>
+                <div class="text1" v-if="info.ownerId">
+                    <img class="text2" :src="info.ownerAvatar" alt="" />
+                    <div class="text3">{{ info.owner }}</div>
+                </div>
+            </div>
+            <div class="text">
+                <!-- <div class="text1">
+                    <img class="text2 text4" src="../assets/img/like.png" alt="" />
+                    <div class="text3">16</div>
+                </div> -->
+                <!-- <like-button :isLike="info.liked" @like="likeProduct">{{ info.likes }}</like-button> -->
+
+                <!-- <div class="text1" v-else>
+                    <img class="text2 text4" src="../assets/img/icon-yuyue@3x.png" alt="" />
+                    <div class="text3">预约</div>
+                </div> -->
+            </div>
+        </div>
+    </router-link>
+</template>
+
+<script>
+import product from '../mixins/product';
+import LikeButton from './LikeButton.vue';
+export default {
+    components: { LikeButton },
+    props: {
+        info: {
+            type: Object,
+            default: () => {
+                return {};
+            }
+        }
+    },
+    mixins: [product],
+    methods: {
+        likeProduct() {
+            if (!this.info.liked) {
+                this.$http.get(`/collection/${this.info.id}/like`).then(() => {
+                    this.$emit('update:info', {
+                        ...this.info,
+                        liked: true,
+                        likes: this.info.likes + 1
+                    });
+                    this.$message.success('收藏成功');
+                });
+            } else {
+                this.$http.get(`/collection/${this.info.id}/unlike`).then(() => {
+                    this.$emit('update:info', {
+                        ...this.info,
+                        liked: false,
+                        likes: this.info.likes - 1
+                    });
+                    this.$message.success('取消收藏');
+                });
+            }
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.collect {
+    .line();
+    width: 276px;
+    height: 416px;
+    display: inline-block;
+    .imgBox {
+        height: 266px;
+        width: 100%;
+        border-radius: 8px 8px 0px 0px;
+    }
+
+    &:hover {
+        .imgBox {
+            position: relative;
+            &::before {
+                content: '';
+                position: absolute;
+                left: 0;
+                top: 0;
+                right: 0;
+                bottom: 0;
+                background-color: fade(@prim, 70%);
+            }
+
+            &::after {
+                padding: 0 22px;
+                content: '查看详情';
+                line-height: 30px;
+                font-size: 16px;
+                color: #1a1a1a;
+                background: #ffffff;
+                border-radius: 16px;
+                border: 1px solid #ffffff;
+                position: absolute;
+                top: 50%;
+                left: 50%;
+                transform: translate(-50%, -50%);
+            }
+        }
+    }
+
+    .introduce {
+        padding: 10px 16px 6px;
+        font-size: 14px;
+        font-weight: 400;
+        color: #939599;
+        line-height: 20px;
+    }
+    .price {
+        display: flex;
+        align-items: center;
+        // justify-content: space-between;
+        padding: 0 16px 16px;
+        .img1 {
+            width: 10px;
+            height: 11px;
+            margin-top: 10px;
+        }
+        .num {
+            font-size: 30px;
+            font-family: OSP-DIN, OSP;
+            font-weight: normal;
+            color: @prim;
+            line-height: 30px;
+        }
+        .time {
+            display: flex;
+            margin-left: 24px;
+            .time1 {
+                font-size: 14px;
+                font-weight: 400;
+                color: #939599;
+                line-height: 24px;
+            }
+            span {
+                font-size: 14px;
+                font-weight: 400;
+                color: @prim;
+                line-height: 24px;
+                margin-left: 6px;
+            }
+        }
+    }
+    .status {
+        font-size: 18px;
+        color: #939599;
+        line-height: 24px;
+        padding: 0 16px 12px;
+    }
+    .border {
+        height: 1px;
+        background: #494a4d;
+        border-radius: 1px;
+        margin: 0 16px;
+    }
+    .fans {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 19px 16px 20px;
+        .text {
+            display: flex;
+            align-items: center;
+            .text1 {
+                display: flex;
+                align-items: center;
+                max-width: 70px;
+                overflow: hidden;
+                &.name1 {
+                    margin-right: 22px;
+                }
+                .text2 {
+                    width: 20px;
+                    height: 20px;
+                    border-radius: 50%;
+                    margin-right: 4px;
+                    flex-shrink: 0;
+                    &.text4 {
+                        width: 18px;
+                        height: 18px;
+                    }
+                }
+                .text3 {
+                    font-size: 14px;
+                    font-weight: 400;
+                    .ellipsis();
+                    color: #939599;
+                    line-height: 24px;
+                }
+            }
+        }
+    }
+}
+</style>

+ 1 - 1
src/main/pc-space/src/components/PageHeader.vue

@@ -79,7 +79,7 @@ export default {
             } else if (item === '收藏探索') {
                 this.$router.push('/collection');
             } else if (item === '数字盲盒') {
-                this.$router.push('/blindbox?flag=' + 1);
+                this.$router.push('/collection?type=BLIND_BOX');
             } else if (item === '我的NFT') {
                 this.$router.push('/my');
             }

+ 10 - 0
src/main/pc-space/src/mixins/product.js

@@ -42,6 +42,13 @@ export default {
             return [...this.changeImgs(this.banners)].map(item => {
                 return this.getImg(item, '', 800);
             });
+        },
+        isAppointment() {
+            if (this.info.startTime) {
+                return dayjs().isBefore(dayjs(this.info.startTime));
+            } else {
+                return false;
+            }
         }
     },
     methods: {
@@ -61,6 +68,9 @@ export default {
             if (!startTime) {
                 return;
             }
+            if (!this.isAppointment) {
+                return;
+            }
             var x = dayjs(startTime);
             var y = dayjs();
             let d = dayjs.duration(x.diff(y));

+ 1 - 1
src/main/pc-space/src/mixins/user.js

@@ -42,7 +42,7 @@ export default {
 
                 this.list = list;
             } else {
-                this.getInfo();
+                this.getDetail();
             }
         }
     }

+ 8 - 0
src/main/pc-space/src/router/index.js

@@ -67,6 +67,14 @@ const routes = [
                     title: '数字盲盒详情'
                 }
             },
+            {
+                path: '/assetDetail',
+                name: 'assetDetail',
+                component: () => import('../views/AssetDetail.vue'),
+                meta: {
+                    title: 'NFT详情'
+                }
+            },
             {
                 path: '/my',
                 name: 'my',

+ 718 - 0
src/main/pc-space/src/views/AssetDetail.vue

@@ -0,0 +1,718 @@
+<template>
+    <div class="container center-content">
+        <div class="top" v-loading="loading">
+            <div class="top-left">
+                <swiper class="mySwiper imgBox" ref="mySwiper" :options="swiperOptions">
+                    <swiper-slide v-for="(item, index) in banners" :key="index">
+                        <video
+                            class="swiper-video"
+                            v-if="isVideo(item)"
+                            :src="item.url"
+                            :poster="getImg(changeImgs([item]), '', 800)"
+                            controls="controls"
+                        >
+                            您的浏览器不支持 video 标签。
+                        </video>
+                        <el-image
+                            v-else
+                            :src="getImg(item.url, '', 800)"
+                            :preview-src-list="bannerList"
+                            style="width: 460px; height: 460px"
+                            fit="scale-down"
+                        />
+                    </swiper-slide>
+                </swiper>
+                <div class="works">
+                    <img class="works1" src="../assets/img/icon-quanyibaohu@3x.png" alt="" />
+                    <div class="works2">该作品已在保存至区块链并进行权益保护</div>
+                </div>
+            </div>
+            <div class="top-info">
+                <div class="title">
+                    <div class="title1">{{ info.name }}</div>
+                    <div class="text">
+                        <like-button size="large" :isLike="info.liked" @like="likeProduct">
+                            {{ info.likes }}
+                        </like-button>
+                        <div class="text1">
+                            <img class="img1" src="../assets/img/fenxiang-icon@3x.png" alt="" />
+                            <div class="text2">分享</div>
+                        </div>
+                    </div>
+                </div>
+                <div class="name-list">
+                    <router-link :to="{ path: '/castingDetail', query: { id: info.minterId } }" class="name">
+                        <img class="name1" :src="info.minterAvatar" alt="" />
+                        <div>
+                            <div class="name2">{{ info.minter }}</div>
+                            <div class="name3">铸造者</div>
+                        </div>
+                        <img class="name4" src="../assets/img/icon_inter@3x (4).png" alt="" />
+                    </router-link>
+                    <div class="name">
+                        <img class="name1" :src="userInfo.avatar" alt="" />
+                        <div>
+                            <div class="name2">{{ userInfo.nickname }}</div>
+                            <div class="name3">持有者</div>
+                        </div>
+                        <img class="name4" src="../assets/img/icon_inter@3x (4).png" alt="" />
+                    </div>
+                </div>
+                <div class="time" v-if="info.salable">
+                    <div class="time1">卖家定价</div>
+                    <div class="time2" v-if="info.isAppointment">
+                        首发抢购倒计时<span>{{ startTime }}</span>
+                    </div>
+                </div>
+                <div class="price" v-if="info.salable">
+                    <div class="price1">
+                        <div class="price2">价格</div>
+                        <img class="price3" src="../assets/img/icon_jiage@3x.png" alt="" />
+                        <div class="price4">{{ info.price }}</div>
+                        <div class="price2" v-if="info.royalties">
+                            含<span>{{ info.royalties }}%</span>版税
+                        </div>
+                    </div>
+                    <div class="price1 num">
+                        <div class="num1">
+                            <div class="price2 num2">数量</div>
+                            <span class="span">{{ info.total }}</span>
+                        </div>
+                        <div class="price2 num2">已售 {{ info.sale }} / 剩余 {{ info.stock }}</div>
+                    </div>
+                    <el-button
+                        class="buy"
+                        :class="{ used: info.appointment }"
+                        v-if="info.isAppointment"
+                        @click="appointment"
+                        type="primary"
+                        size="default"
+                    >
+                        {{ info.appointment ? '已预约' : '一键预约' }}
+                    </el-button>
+
+                    <el-button class="buy" v-else-if="isBuy" type="primary" size="default">立即购买</el-button>
+                </div>
+                <div v-if="properties.length > 0">
+                    <div class="time">
+                        <div class="time1">商品特性</div>
+                    </div>
+                    <div class="card">
+                        <div class="box1" v-for="(item, index) in properties" :key="index">
+                            <div class="box2">{{ item.name }}</div>
+                            <div class="box3">{{ item.value }}</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="content" v-loading="loading">
+            <el-collapse v-model="activeNames" :accordion="false">
+                <el-collapse-item name="1" v-if="info.tokenId">
+                    <template slot="title">
+                        <img class="imgs" src="../assets/icon-lianshangxinxi.png" alt="" />
+                        <span>链上信息</span>
+                    </template>
+                    <div class="info4">
+                        <div class="text1">Hash地址:{{ info.txHash }}</div>
+                        <div class="text1">区块高度: {{ info.blockNumber }}</div>
+                        <div class="text1">令牌ID: {{ info.tokenId }}</div>
+                    </div>
+                </el-collapse-item>
+
+                <el-collapse-item name="2">
+                    <template slot="title">
+                        <img class="imgs" src="../assets/icon-miaoshu.png" alt="" />
+                        <span>作品描述</span>
+                    </template>
+                    <div class="info4">
+                        <swiper class="boxs" :options="boxOptions">
+                            <swiper-slide v-for="(item, index) in boxs" :key="index">
+                                <el-image :src="item" :preview-src-list="boxs" fit="scale-down" />
+                            </swiper-slide>
+                        </swiper>
+                        <div v-html="info.detail"></div>
+                    </div>
+                </el-collapse-item>
+
+                <!-- <el-collapse-item name="3">
+                    <template slot="title">
+                        <img class="imgs" src="../assets/info_icon_jiaoyijilu.png" alt="" />
+                        <span>交易历史</span>
+                    </template>
+                    <el-table :data="tableData" stripe style="width: 100%">
+                        <el-table-column prop="date" label="日期" width="180"> </el-table-column>
+                        <el-table-column prop="name" label="姓名" width="180"> </el-table-column>
+                        <el-table-column prop="address" label="地址"> </el-table-column>
+                    </el-table>
+                </el-collapse-item> -->
+            </el-collapse>
+        </div>
+        <div class="nft" v-if="relateds.length > 0">
+            <div class="nft1">
+                <img class="nft2" :src="info.minterAvatar" alt="" />
+                <div class="nft3">{{ info.minter }}</div>
+            </div>
+            <div class="nft4">来自创作者的NFT</div>
+            <router-link class="nft1" :to="{ path: '/castingDetail', query: { id: info.minterId } }">
+                <div class="nft5">查看更多</div>
+                <img class="nft6" src="../assets/img/icon_inter@3x (4).png" alt="" />
+            </router-link>
+        </div>
+        <div class="list" v-loading="loading" v-if="relateds.length > 0">
+            <collection-info v-for="(item, index) in relateds" :key="item.id" :info.sync="relateds[index]">
+            </collection-info>
+        </div>
+
+        <submit></submit>
+    </div>
+</template>
+<script>
+import { Swiper, SwiperSlide } from 'vue-awesome-swiper';
+import product from '../mixins/product';
+import 'swiper/css/swiper.css';
+import LikeButton from '../components/LikeButton.vue';
+import CollectionInfo from '../components/CollectionInfo.vue';
+import Submit from './Submit.vue';
+import { mapState } from 'vuex';
+export default {
+    components: { Swiper, SwiperSlide, LikeButton, CollectionInfo, Submit },
+    mixins: [product],
+    data() {
+        return {
+            showMore: false,
+            showMore1: false,
+            showMore2: false,
+            tableData: [],
+            info: {},
+            blindBoxItems: [],
+            swiperOptions: {},
+            activeNames: ['2', '3'],
+            relateds: [],
+            boxOptions: { slidesPerView: 4, spaceBetween: 24, autoplay: true },
+            loading: true
+        };
+    },
+    computed: {
+        ...mapState(['userInfo']),
+        banners() {
+            return this.info.pic || [];
+        },
+        properties() {
+            return this.info.properties || [];
+        },
+        isBuy() {
+            return this.info.stock && this.info.onShelf && this.info.salable;
+        },
+        boxs() {
+            let list = [...this.blindBoxItems];
+            return list.map(item => {
+                return this.getImg(this.changeImgs(item.pics));
+            });
+        }
+    },
+    mounted() {
+        this.getDetail();
+    },
+    watch: {
+        $route() {
+            this.getDetail();
+        }
+    },
+    methods: {
+        getDetail() {
+            this.$http
+                .get('/asset/get/' + this.$route.query.id)
+                .then(res => {
+                    this.info = res;
+                    this.loading = false;
+                    this.getTime(res.startTime);
+                    this.getRelated(res.ownerId);
+                    if (res.type === 'BLIND_BOX') {
+                        return this.$http.post(
+                            '/blindBoxItem/all',
+                            {
+                                query: {
+                                    blindBoxId: res.id
+                                }
+                            },
+                            { body: 'json' }
+                        );
+                    } else {
+                        return Promise.resolve();
+                    }
+                })
+                .then(res => {
+                    if (res) {
+                        this.blindBoxItems = res.content;
+                    } else {
+                        this.blindBoxItems = [];
+                    }
+                });
+        },
+        likeProduct() {
+            if (!this.info.liked) {
+                this.$http.get(`/collection/${this.info.id}/like`).then(() => {
+                    this.getDetail();
+                    this.$message.success('收藏成功');
+                });
+            } else {
+                this.$http.get(`/collection/${this.info.id}/unlike`).then(() => {
+                    this.getDetail();
+                    this.$message.success('取消收藏');
+                });
+            }
+        },
+        getRelated(id) {
+            this.$http
+                .post(
+                    '/collection/all',
+                    {
+                        query: {
+                            del: false,
+                            ownerId: id
+                        },
+                        size: 5,
+                        sort: 'createdAt,desc'
+                    },
+                    {
+                        body: 'json'
+                    }
+                )
+                .then(res => {
+                    let list = [];
+                    res.content.forEach(item => {
+                        if (item.id !== this.info.id && list.length < 4) {
+                            list.push(item);
+                        }
+                    });
+                    this.relateds = list;
+                });
+        },
+        appointment() {
+            this.$http
+                .post('/collection/appointment?id=' + this.info.id)
+                .then(res => {
+                    this.getDetail();
+                    this.$message.success('预约成功');
+                })
+                .catch(e => {
+                    if (e.error) {
+                        this.$message.warning(e.error);
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.container {
+    padding: 50px 50px 200px;
+    background: #1a1a1a;
+    .top {
+        display: flex;
+
+        .top-left {
+            width: 460px;
+            flex-shrink: 0;
+        }
+
+        .top-info {
+            flex-grow: 1;
+            margin-left: 30px;
+        }
+
+        .works {
+            display: flex;
+            align-items: center;
+            height: 68px;
+            background: #1c1e26;
+            border-radius: 8px;
+            margin-top: 30px;
+            .works1 {
+                width: 24px;
+                height: 24px;
+                margin: 2px 10px 0 16px;
+            }
+            .works2 {
+                font-size: 16px;
+                font-weight: 400;
+                color: #ffffff;
+            }
+        }
+        .title {
+            display: flex;
+            margin-top: 10px;
+            padding-right: 100px;
+            position: relative;
+            .title1 {
+                font-size: 26px;
+                font-weight: bold;
+                color: #ffffff;
+                .ellipsis();
+            }
+            .text {
+                position: absolute;
+                right: 24px;
+                top: -10px;
+                display: flex;
+                .text1 {
+                    text-align: center;
+                    margin-left: 16px;
+                    cursor: pointer;
+                    display: inline-flex;
+                    align-items: center;
+                    flex-direction: column;
+                    .img1 {
+                        width: 24px;
+                        height: 24px;
+                        display: block;
+                    }
+                    .text2 {
+                        font-size: 16px;
+                        font-weight: 400;
+                        color: #949699;
+                        line-height: 24px;
+                        margin-top: 4px;
+                    }
+                }
+            }
+        }
+        .name-list {
+            display: flex;
+            padding: 12px 0;
+        }
+        .name {
+            display: flex;
+            align-items: center;
+            height: 64px;
+            line-height: 64px;
+            cursor: pointer;
+            margin-right: 80px;
+
+            .name1 {
+                width: 38px;
+                height: 38px;
+                border-radius: 50%;
+                margin-right: 12px;
+            }
+            .name2 {
+                font-size: 14px;
+                font-weight: 400;
+                color: #ffffff;
+                line-height: 24px;
+            }
+            .name3 {
+                font-size: 12px;
+                font-weight: 400;
+                color: #939599;
+                line-height: 22px;
+            }
+            .name4 {
+                width: 24px;
+                height: 24px;
+                margin-left: 16px;
+            }
+        }
+        .time {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            background: #1c1e26;
+            border-radius: 8px 8px 0px 0px;
+            // width: 678px;
+            height: 56px;
+            padding: 0 16px;
+            .time1 {
+                font-size: 18px;
+                font-weight: bold;
+                color: #ffffff;
+                line-height: 24px;
+            }
+            .time2 {
+                font-size: 16px;
+                font-weight: 400;
+                color: #939599;
+                line-height: 24px;
+            }
+            span {
+                font-size: 16px;
+                font-weight: 400;
+                color: @prim;
+                line-height: 24px;
+                margin-left: 6px;
+            }
+        }
+        .price {
+            // width: 678px;
+            // height: 220px;
+            padding: 0 16px;
+            background: #1f2230;
+            border-radius: 0px 0px 8px 8px;
+            margin-bottom: 30px;
+            .price1 {
+                display: flex;
+                padding: 18px 0 24px 0;
+                border-bottom: 1px solid #494a4d;
+                &.num {
+                    justify-content: space-between;
+                    border-bottom: 0;
+                }
+                .num1 {
+                    display: flex;
+                    align-items: center;
+                }
+                .span {
+                    font-size: 14px;
+                    font-weight: bold;
+                    color: #ffffff;
+                    line-height: 22px;
+                    margin-left: 16px;
+                }
+                .price2 {
+                    font-size: 16px;
+                    font-weight: 400;
+                    color: #939599;
+                    line-height: 24px;
+                    padding-top: 12px;
+                    &.num2 {
+                        padding-top: 0;
+                    }
+                }
+                .price3 {
+                    width: 10px;
+                    height: 11px;
+                    margin: 18px 0 0 10px;
+                }
+                .price4 {
+                    font-size: 40px;
+                    font-weight: normal;
+                    color: @prim;
+                    line-height: 36px;
+                    margin-right: 5px;
+                }
+                span {
+                    font-size: 16px;
+                    font-weight: 400;
+                    color: @prim;
+                    line-height: 24px;
+                }
+            }
+            .btn {
+                height: 52px;
+                line-height: 52px;
+                text-align: center;
+                background: linear-gradient(135deg, @prim 0%, @warn 100%);
+                border-radius: 8px;
+            }
+        }
+        .card {
+            // width: 590px;
+            height: 112px;
+            border-radius: 0px 0px 8px 8px;
+            background: #1f2230;
+            padding: 0 60px;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            .box1 {
+                width: 120px;
+                height: 62px;
+                text-align: center;
+                border-radius: 4px;
+                border: 1px solid;
+                border-image: linear-gradient(135deg, rgba(0, 255, 203, 1), rgba(0, 110, 255, 1)) 1 1;
+                .box2 {
+                    font-size: 14px;
+                    font-weight: 400;
+                    color: #939599;
+                    line-height: 24px;
+                    margin-top: 5px;
+                }
+                .box3 {
+                    font-size: 14px;
+                    font-weight: 400;
+                    color: #ffffff;
+                    line-height: 24px;
+                }
+            }
+        }
+    }
+    .content {
+        .info {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 0 16px;
+            background: #1c1e26;
+            &.rad {
+                border-radius: 8px 8px 0px 0px;
+                margin-top: 30px;
+            }
+            .info1 {
+                display: flex;
+                align-items: center;
+                height: 68px;
+                .imgs {
+                    width: 24px;
+                    height: 24px;
+                    margin-right: 10px;
+                }
+                .info2 {
+                    font-size: 16px;
+                    font-weight: 400;
+                    color: #ffffff;
+                    line-height: 24px;
+                }
+            }
+
+            .info3 {
+                width: 24px;
+                height: 24px;
+            }
+        }
+        .border {
+            height: 1px;
+            background: #494a4d;
+            border-radius: 1px;
+            margin: 0 16px;
+        }
+        .info4 {
+            color: #fff;
+            padding: 16px;
+        }
+    }
+    .nft {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 50px 0 27px;
+        .nft1 {
+            display: flex;
+            align-items: center;
+            .nft2 {
+                width: 38px;
+                height: 38px;
+                border-radius: 50%;
+                margin-right: 12px;
+            }
+            .nft3 {
+                font-size: 16px;
+                font-weight: 400;
+                color: #ffffff;
+                line-height: 24px;
+            }
+            .nft5 {
+                font-size: 13px;
+                font-weight: 400;
+                color: #939599;
+                line-height: 18px;
+                margin-top: 2px;
+            }
+            .nft6 {
+                width: 24px;
+                height: 24px;
+            }
+        }
+        .nft4 {
+            font-size: 18px;
+            font-weight: bold;
+            color: #ffffff;
+            line-height: 30px;
+        }
+    }
+}
+.mySwiper {
+    width: 462px;
+    display: inline-block;
+    .line();
+    /deep/.el-image {
+        display: block;
+        img {
+            display: block;
+        }
+    }
+}
+.buy {
+    width: 100%;
+    display: block;
+    height: 52px;
+    background: linear-gradient(135deg, @prim 0%, @warn 100%);
+    border-radius: 8px;
+    border-width: 0;
+    color: #000;
+    &:hover {
+        background: linear-gradient(135deg, darken(@prim, 10%), darken(@warn, 10%));
+    }
+
+    &.used {
+        background: linear-gradient(135deg, darken(@prim, 50%), darken(@warn, 50%));
+        color: #939599;
+    }
+}
+
+/deep/.content {
+    margin-top: 30px;
+    .el-collapse-item__wrap {
+        // border-bottom: 1px solid #494a4d;
+        border-bottom-width: 0;
+    }
+    .el-collapse {
+        border-width: 0;
+    }
+
+    .el-collapse-item__header {
+        background-color: #1c1e26;
+        padding-left: 16px;
+        height: 68px;
+        border-bottom-width: 0;
+        img {
+            width: 24px;
+            height: 24px;
+            margin-right: 10px;
+        }
+        font-size: 16px;
+        color: #ffffff;
+        line-height: 24px;
+    }
+    .el-collapse-item__content {
+        background-color: #1f2230;
+    }
+    .el-table {
+        background-color: transparent;
+        padding: 0 16px;
+        tr {
+            background-color: transparent;
+        }
+        th {
+            background-color: transparent;
+        }
+        &::before {
+            content: none;
+        }
+    }
+
+    .el-collapse-item {
+        &:last-child {
+            .el-collapse-item__wrap {
+                border-width: 0;
+            }
+        }
+    }
+}
+.list {
+    margin: -16px;
+
+    .collect {
+        margin: 16px;
+    }
+}
+
+/deep/.boxs {
+    .el-image {
+        width: 100%;
+        height: 310px;
+        border-radius: 12px;
+    }
+}
+</style>

+ 12 - 4
src/main/pc-space/src/views/CastingDetail.vue

@@ -22,7 +22,9 @@
                         <span>{{ info.followers }}</span>
                     </div>
 
-                    <el-button plain round class="prim" size="mini">关注</el-button>
+                    <el-button plain round :class="{ prim: !info.follow }" size="mini" @click="like(info)">
+                        {{ info.follow ? '已关注' : '关注' }}
+                    </el-button>
                     <el-popover placement="bottom" trigger="hover">
                         <el-button style="margin-left: 20px" slot="reference" plain round size="mini">分享</el-button>
                         <share :info="info"> </share>
@@ -62,7 +64,11 @@
             </div>
 
             <div class="list" v-loading="fetchingData">
-                <collection-info v-for="item in list" :key="item.id" :info="item"></collection-info>
+                <collection-info
+                    v-for="(item, index) in list"
+                    :key="item.id"
+                    :info.sync="list[index]"
+                ></collection-info>
 
                 <el-empty v-if="empty" description="还没有该类型的藏品哦~"></el-empty>
             </div>
@@ -87,9 +93,10 @@
 import CollectionInfo from '../components/CollectionInfo.vue';
 import Share from '../components/Share.vue';
 import pageableTable from '../mixins/pageableTable';
+import user from '../mixins/user';
 export default {
     components: { CollectionInfo, Share },
-    mixins: [pageableTable],
+    mixins: [pageableTable, user],
     data() {
         return {
             info: {},
@@ -124,7 +131,8 @@ export default {
             search: '',
             url: '/collection/all',
             list: [],
-            isOwner: true
+            isOwner: true,
+            isList: false
         };
     },
     mounted() {

+ 36 - 3
src/main/pc-space/src/views/Collection.vue

@@ -1,7 +1,7 @@
 <template>
     <div class="container">
-        <div class="title" ref="anchor">欢迎来到 NFT 市场</div>
-        <el-radio-group class="menu" v-model="select" size="normal">
+        <div class="title" ref="anchor">{{ type === 'BLIND_BOX' ? '欢迎来到 NFT 盲盒市场' : '欢迎来到 NFT 市场' }}</div>
+        <el-radio-group v-if="type !== 'BLIND_BOX'" class="menu" v-model="select" size="normal">
             <el-radio-button v-for="(item, index) in typeList" :key="index" :label="item.value">
                 <div class="radio-item">
                     <i class="font_family" :class="[item.icon]"></i>
@@ -10,7 +10,7 @@
             </el-radio-button>
         </el-radio-group>
 
-        <div class="border" style="margin-top: 30px"></div>
+        <div class="border" v-if="type !== 'BLIND_BOX'" style="margin-top: 30px"></div>
         <div class="search-list">
             <el-input
                 class="search"
@@ -135,9 +135,42 @@ export default {
 
             this.page = 1;
             this.getData();
+        },
+        '$route.query.type'() {
+            if (!this.$route.query.type || this.$route.query.type === 'BLIND_BOX') {
+                this.init();
+            }
         }
     },
+    mounted() {
+        this.init();
+    },
     methods: {
+        init() {
+            this.type = 'DEFAULT,AUCTION';
+            this.search = '';
+            this.select = '0';
+            this.sortStr = 'createdAt,desc';
+            this.page = 1;
+            if (this.$route.query.sort) {
+                this.sortStr = this.$route.query.sort;
+            }
+            if (this.$route.query.type) {
+                this.type = this.$route.query.type;
+            }
+            if (this.type === 'DEFAULT') {
+                this.select = '1';
+            }
+            if (this.type === 'AUCTION') {
+                this.select = '3';
+            }
+            if (this.$route.query.canResale) {
+                this.canResale = true;
+                this.select = '2';
+            }
+            this.isFirst = true;
+            this.getData();
+        },
         beforeGetData() {
             if (this.canResale) {
                 return {

+ 30 - 3
src/main/pc-space/src/views/CollectionDetail.vue

@@ -60,7 +60,7 @@
                 </div>
                 <div class="time" v-if="info.salable">
                     <div class="time1">卖家定价</div>
-                    <div class="time2" v-if="info.startTime">
+                    <div class="time2" v-if="info.isAppointment">
                         首发抢购倒计时<span>{{ startTime }}</span>
                     </div>
                 </div>
@@ -80,7 +80,16 @@
                         </div>
                         <div class="price2 num2">已售 {{ info.sale }} / 剩余 {{ info.stock }}</div>
                     </div>
-                    <el-button class="buy" v-if="info.startTime" type="primary" size="default">一键预约</el-button>
+                    <el-button
+                        class="buy"
+                        :class="{ used: info.appointment }"
+                        v-if="info.isAppointment"
+                        @click="appointment"
+                        type="primary"
+                        size="default"
+                    >
+                        {{ info.appointment ? '已预约' : '一键预约' }}
+                    </el-button>
 
                     <el-button class="buy" v-else-if="isBuy" type="primary" size="default">立即购买</el-button>
                 </div>
@@ -277,6 +286,19 @@ export default {
                     });
                     this.relateds = list;
                 });
+        },
+        appointment() {
+            this.$http
+                .post('/collection/appointment?id=' + this.info.id)
+                .then(res => {
+                    this.getDetail();
+                    this.$message.success('预约成功');
+                })
+                .catch(e => {
+                    if (e.error) {
+                        this.$message.warning(e.error);
+                    }
+                });
         }
     }
 };
@@ -620,6 +642,11 @@ export default {
     &:hover {
         background: linear-gradient(135deg, darken(@prim, 10%), darken(@warn, 10%));
     }
+
+    &.used {
+        background: linear-gradient(135deg, darken(@prim, 50%), darken(@warn, 50%));
+        color: #939599;
+    }
 }
 
 /deep/.content {
@@ -674,7 +701,7 @@ export default {
 .list {
     margin: -16px;
 
-    .content {
+    .collect {
         margin: 16px;
     }
 }

+ 106 - 72
src/main/pc-space/src/views/My.vue

@@ -1,96 +1,130 @@
 <template>
     <div class="container center-content">
         <div class="title">我的 NFT</div>
-        <div class="tabs">
-            <div
-                class="tab"
-                :class="{ active: active === item }"
-                v-for="(item, index) in tabs"
-                :key="index"
-                @click="tab(item)"
+        <el-radio-group class="menu" v-model="select" size="normal">
+            <el-radio-button v-for="(item, index) in typeList" :key="index" :label="item.value">
+                <div class="radio-item">
+                    <!-- <i class="font_family" :class="[item.icon]"></i> -->
+                    <span>{{ item.label }}</span>
+                </div>
+            </el-radio-button>
+        </el-radio-group>
+        <div class="border" style="margin-top: 30px"></div>
+        <div class="search-list">
+            <el-input
+                class="search"
+                prefix-icon="el-icon-search"
+                placeholder="请输入您想找到的作品名称…"
+                v-model="search"
+                clearable
+                @change="onSearch"
             >
-                {{ item }}
-            </div>
+            </el-input>
+            <el-select class="select" v-model="sortStr" placeholder="请选择">
+                <el-option v-for="item in sortList" :key="item.value" :label="item.label" :value="item.value">
+                </el-option>
+            </el-select>
         </div>
-        <div class="border"></div>
-        <search-info></search-info>
-        <goods-info></goods-info>
-        <!-- <div>
+        <div class="list" v-loading="fetchingData">
+            <asset-info v-for="(item, index) in list" :key="item.id" :info.sync="list[index]"></asset-info>
+
+            <el-empty v-if="empty" description="还没有该类型的藏品哦~"></el-empty>
+        </div>
+
+        <div class="pagination-wrapper">
             <el-pagination
-                @size-change="handleSizeChange"
-                @current-change="handleCurrentChange"
-                :current-page="currentPage4"
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
                 :page-sizes="[10, 20, 30, 40, 50]"
-                :page-size="100"
-                layout="total, sizes, prev, pager, next, jumper"
-                :total="400"
+                :page-size="pageSize"
+                layout="total, prev, pager, next"
+                :total="totalElements"
             >
             </el-pagination>
-        </div> -->
+        </div>
     </div>
 </template>
 <script>
-import GoodsInfo from '../components/GoodsInfo.vue';
-import SearchInfo from '../components/SearchInfo.vue';
+import AssetInfo from '../components/AssetInfo.vue';
+import pageableTable from '../mixins/pageableTable';
 export default {
-    components: { GoodsInfo, SearchInfo },
+    components: { AssetInfo },
+    mixins: [pageableTable],
     data() {
         return {
-            tabs: ['我拥有的(10)', '我卖出的(5)', '我铸造的(8)'],
-            active: '我拥有的(10)',
-            currentPage4: 4
+            typeList: [
+                {
+                    label: '我拥有的',
+                    icon: 'icon-icon-quanbu',
+                    value: '0'
+                },
+                {
+                    label: '我卖出的',
+                    icon: 'icon-icon-zuixin',
+                    value: '1'
+                },
+                {
+                    label: '藏品兑换券',
+                    icon: 'icon-icon-zhuanrang',
+                    value: '2'
+                }
+            ],
+            sortList: [
+                {
+                    label: '综合排序',
+                    value: 'createdAt,desc'
+                },
+                {
+                    label: '热门排序',
+                    value: 'likes,desc'
+                },
+                {
+                    label: '价格降序',
+                    value: 'price,desc'
+                },
+                {
+                    label: '价格升序',
+                    value: 'price,asc'
+                }
+            ],
+            select: '0',
+            url: '/asset/all',
+            search: '',
+            list: []
         };
     },
+    mounted() {
+        this.init();
+    },
     methods: {
-        tab(item) {
-            this.active = item;
+        init() {
+            this.checkLogin()
+                .then(res => {
+                    this.page = 1;
+                    this.sortStr = 'createdAt,desc';
+                    this.search = '';
+                    this.getData();
+                })
+                .catch(() => {
+                    this.fetchingData = false;
+                    this.empty = true;
+                });
+        },
+        beforeGetData() {
+            return {
+                search: this.search,
+                query: {
+                    userId: this.$store.state.userInfo.id
+                }
+            };
+        },
+        setList(list) {
+            this.list = list;
         }
     }
 };
 </script>
 <style lang="less" scoped>
-.container {
-    .title {
-        height: 42px;
-        font-size: 32px;
-        font-weight: 400;
-        color: #ffffff;
-        line-height: 42px;
-        padding: 60px 0;
-    }
-    .tabs {
-        display: flex;
-        align-items: center;
-        padding-bottom: 30px;
-        text-align: center;
-        .tab {
-            width: 140px;
-            border: 1px solid #939599;
-            height: 42px;
-            font-size: 18px;
-            font-weight: bold;
-            color: #949699;
-            line-height: 42px;
-            &.active {
-                color: #ffffff;
-                background: linear-gradient(46deg, @prim 0%, @warn 100%);
-            }
-            &:first-child {
-                border-radius: 8px 0px 0px 8px;
-            }
-            &:last-child {
-                border-radius: 0px 8px 8px 0px;
-            }
-        }
-    }
-    .border {
-        height: 1px;
-        background: #494a4d;
-        border-radius: 1px;
-    }
-
-    // .footer {
-    //     flex-shrink: 0;
-    // }
-}
+@import url('../styles/list.less');
 </style>

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

@@ -0,0 +1,27 @@
+package com.izouma.nineth.service;
+
+import com.izouma.nineth.ApplicationTests;
+import com.izouma.nineth.domain.BlindBoxItem;
+import com.izouma.nineth.domain.Order;
+import com.izouma.nineth.repo.BlindBoxItemRepo;
+import com.izouma.nineth.repo.OrderRepo;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AssetServiceTest extends ApplicationTests {
+    @Autowired
+    private OrderRepo        orderRepo;
+    @Autowired
+    private BlindBoxItemRepo blindBoxItemRepo;
+    @Autowired
+    private AssetService     assetService;
+
+    @Test
+    void createAsset() {
+        BlindBoxItem item = blindBoxItemRepo.findById(1860L).get();
+        Order order = orderRepo.findById(1922L).get();
+        assetService.createAsset(order, item);
+    }
+}