xiongzhu před 3 roky
rodič
revize
2db1b22c80

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

@@ -22,6 +22,7 @@ import org.hibernate.annotations.DynamicUpdate;
 import javax.persistence.*;
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.time.LocalDateTime;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -223,6 +224,10 @@ public class Asset extends CollectionBaseEntity {
     @Transient
     private int num;
 
+    private LocalDateTime lockAt;
+
+    private LocalDateTime lockTo;
+
     public static Asset create(Collection collection, User user) {
         return Asset.builder()
                 .userId(user.getId())

+ 36 - 0
src/main/java/com/izouma/nineth/domain/AssetLock.java

@@ -0,0 +1,36 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class AssetLock extends BaseEntity {
+
+    private Long userId;
+
+    private String phone;
+
+    private String nickname;
+
+    private Long assetId;
+
+    private String name;
+
+    private Integer number;
+
+    private LocalDateTime lockAt;
+
+    private LocalDateTime lockTo;
+
+    private Duration duration;
+}

+ 16 - 0
src/main/java/com/izouma/nineth/repo/AssetLockRepo.java

@@ -0,0 +1,16 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.AssetLock;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import javax.transaction.Transactional;
+
+public interface AssetLockRepo extends JpaRepository<AssetLock, Long>, JpaSpecificationExecutor<AssetLock> {
+    @Query("update AssetLock t set t.del = true where t.id = ?1")
+    @Modifying
+    @Transactional
+    void softDelete(Long id);
+}

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

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

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

@@ -34,6 +34,7 @@ import org.springframework.stereotype.Service;
 
 import javax.persistence.criteria.Predicate;
 import java.math.BigDecimal;
+import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
@@ -65,6 +66,7 @@ public class AssetService {
     private HCChainService          hcChainService;
     private RockRecordService       rockRecordService;
     private RockRecordRepo          rockRecordRepo;
+    private AssetLockRepo           assetLockRepo;
 
 
     public Page<Asset> all(PageQuery pageQuery) {
@@ -137,6 +139,7 @@ public class AssetService {
         asset.setOasisId(collection.getOasisId());
         asset.setOrderId(orderId);
         asset.setPrice(price);
+        asset.setPrefixName(collection.getPrefixName());
         asset.setTags(new HashSet<>());
         if (collection.getTags() != null) {
             asset.getTags().addAll(collection.getTags());
@@ -243,6 +246,9 @@ public class AssetService {
         if (!asset.getUserId().equals(SecurityUtils.getAuthenticatedUser().getId())) {
             throw new BusinessException("此藏品不属于你");
         }
+        if (asset.getLockTo() != null && asset.getLockTo().isAfter(LocalDateTime.now())) {
+            throw new BusinessException("已锁仓,不能上架展示");
+        }
         if (asset.isPublicShow()) {
             return;
         }
@@ -294,6 +300,10 @@ public class AssetService {
             throw new BusinessException("此藏品不属于你");
         }
 
+        if (asset.getLockTo() != null && asset.getLockTo().isAfter(LocalDateTime.now())) {
+            throw new BusinessException("已锁仓,不能寄售");
+        }
+
         int holdDays;
         if (asset.getSource() == AssetSource.GIFT) {
             holdDays = sysConfigService.getInt("gift_days");
@@ -882,4 +892,42 @@ public class AssetService {
                     });
         }).get();
     }
+
+    public void lockAsset(Long userId, Long assetId, Duration duration) {
+        User user = userRepo.findById(userId).orElseThrow(new BusinessException("用户不存在"));
+        Asset asset = assetRepo.findById(assetId).orElseThrow(new BusinessException("藏品不存在"));
+        if (!asset.getUserId().equals(userId)) {
+            throw new BusinessException("无权限");
+        }
+        if (asset.getLockTo() != null && asset.getLockTo().isAfter(LocalDateTime.now())) {
+            throw new BusinessException("已是锁仓状态");
+        }
+        if (asset.getType() == CollectionType.SHOWROOM) {
+            throw new BusinessException("展厅不可锁定");
+        }
+        if (asset.getStatus() != AssetStatus.NORMAL) {
+            throw new BusinessException("当前状态不可锁定");
+        }
+        if (asset.isPublicShow() || asset.isConsignment()) {
+            throw new BusinessException("请先取消展示和寄售");
+        }
+        if (duration.compareTo(Duration.parse("P1D")) < 0) {
+            throw new BusinessException("最小锁定1天");
+        }
+        asset.setLockAt(LocalDateTime.now());
+        asset.setLockTo(asset.getLockAt().plus(duration));
+        assetRepo.saveAndFlush(asset);
+
+        assetLockRepo.save(AssetLock.builder()
+                .userId(userId)
+                .phone(user.getPhone())
+                .nickname(user.getNickname())
+                .assetId(assetId)
+                .name(asset.getName())
+                .number(asset.getNumber())
+                .lockAt(asset.getLockAt())
+                .lockTo(asset.getLockTo())
+                .duration(duration)
+                .build());
+    }
 }

+ 15 - 2
src/main/java/com/izouma/nineth/service/GiftOrderService.java

@@ -78,7 +78,9 @@ public class GiftOrderService {
         if (!asset.getUserId().equals(userId)) {
             throw new BusinessException("无权限");
         }
-
+        if (asset.getLockTo() != null && asset.getLockTo().isAfter(LocalDateTime.now())) {
+            throw new BusinessException("已锁仓,不能转赠");
+        }
         User user = userRepo.findById(asset.getUserId()).orElseThrow(new BusinessException("用户不存在"));
         if (!passwordEncoder.matches(tradeCode, user.getTradeCode())) {
             throw new BusinessException("交易密码错误");
@@ -147,7 +149,9 @@ public class GiftOrderService {
         if (!asset.getUserId().equals(userId)) {
             throw new BusinessException("无权限");
         }
-
+        if (asset.getLockTo() != null && asset.getLockTo().isAfter(LocalDateTime.now())) {
+            throw new BusinessException("已锁仓,不能转赠");
+        }
         User user = userRepo.findById(asset.getUserId()).orElseThrow(new BusinessException("用户不存在"));
         if (!passwordEncoder.matches(tradeCode, user.getTradeCode())) {
             throw new BusinessException("交易密码错误");
@@ -160,6 +164,15 @@ public class GiftOrderService {
             holdDays = asset.getHoldDays();
         }
 
+        if (holdDays == 0 && AssetSource.OFFICIAL.equals(asset.getSource())) {
+            BigDecimal officialConsignment = sysConfigService.getBigDecimal("OFFICIAL_CONSIGNMENT");
+            //天转小时
+            int hour = officialConsignment.multiply(new BigDecimal("24")).intValue();
+            if (ChronoUnit.HOURS.between(asset.getCreatedAt(), LocalDateTime.now()) < hour) {
+                throw new BusinessException("需持有满" + hour + "小时后才能转赠");
+            }
+        }
+
         if (ChronoUnit.DAYS.between(asset.getCreatedAt(), LocalDateTime.now()) < holdDays) {
             throw new BusinessException("需持有满" + holdDays + "天才能转赠");
         }

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

@@ -27,6 +27,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.math.BigDecimal;
+import java.time.Duration;
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
@@ -224,6 +225,11 @@ public class AssetController extends BaseController {
     public void hcChain() throws ExecutionException, InterruptedException {
         assetService.hcChain();
     }
+
+    @PostMapping("/lockAsset")
+    public void lockAsset(@RequestParam Long assetId, @RequestParam Duration duration) {
+        assetService.lockAsset(SecurityUtils.getAuthenticatedUser().getId(), assetId, duration);
+    }
 }
 
 

+ 60 - 0
src/main/java/com/izouma/nineth/web/AssetLockController.java

@@ -0,0 +1,60 @@
+package com.izouma.nineth.web;
+import com.izouma.nineth.domain.AssetLock;
+import com.izouma.nineth.service.AssetLockService;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.AssetLockRepo;
+import com.izouma.nineth.utils.ObjUtils;
+import com.izouma.nineth.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/assetLock")
+@AllArgsConstructor
+public class AssetLockController extends BaseController {
+    private AssetLockService assetLockService;
+    private AssetLockRepo assetLockRepo;
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public AssetLock save(@RequestBody AssetLock record) {
+        if (record.getId() != null) {
+            AssetLock orig = assetLockRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+            ObjUtils.merge(orig, record);
+            return assetLockRepo.save(orig);
+        }
+        return assetLockRepo.save(record);
+    }
+
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/all")
+    public Page<AssetLock> all(@RequestBody PageQuery pageQuery) {
+        return assetLockService.all(pageQuery);
+    }
+
+    @GetMapping("/get/{id}")
+    public AssetLock get(@PathVariable Long id) {
+        return assetLockRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        assetLockRepo.softDelete(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<AssetLock> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+}
+

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

@@ -0,0 +1 @@
+{"tableName":"AssetLock","className":"AssetLock","remark":"锁仓","genTable":true,"genClass":true,"genList":true,"genForm":true,"genRouter":true,"javaPath":"/Users/drew/Projects/Java/raex_back/src/main/java/com/izouma/nineth","viewPath":"/Users/drew/Projects/Java/raex_back/src/main/vue/src/views","routerPath":"/Users/drew/Projects/Java/raex_back/src/main/vue/src","resourcesPath":"/Users/drew/Projects/Java/raex_back/src/main/resources","dataBaseType":"Mysql","fields":[{"name":"userId","modelName":"userId","remark":"用户ID","showInList":true,"showInForm":true,"formType":"number"},{"name":"phone","modelName":"phone","remark":"手机","showInList":true,"showInForm":true,"formType":"singleLineText"},{"name":"nickname","modelName":"nickname","remark":"昵称","showInList":true,"showInForm":true,"formType":"singleLineText"},{"name":"assetId","modelName":"assetId","remark":"藏品ID","showInList":true,"showInForm":true,"formType":"number"},{"name":"name","modelName":"name","remark":"藏品名称","showInList":true,"showInForm":true,"formType":"singleLineText"},{"name":"number","modelName":"number","remark":"藏品编号","showInList":true,"showInForm":true,"formType":"number"},{"name":"lockAt","modelName":"lockAt","remark":"起始时间","showInList":true,"showInForm":true,"formType":"datetime"},{"name":"lockTo","modelName":"lockTo","remark":"结束时间","showInList":true,"showInForm":true,"formType":"datetime"}],"readTable":false,"dataSourceCode":"dataSource","genJson":"","subtables":[],"update":false,"basePackage":"com.izouma.nineth","tablePackage":"com.izouma.nineth.domain.AssetLock"}

+ 1 - 0
src/main/vue/package.json

@@ -19,6 +19,7 @@
     "clipboard": "^2.0.6",
     "core-js": "^3.6.5",
     "date-fns": "^2.14.0",
+    "duration-fns": "^3.0.1",
     "element-ui": "^2.15.1",
     "faker": "^5.5.3",
     "jdenticon": "^3.1.0",

+ 16 - 0
src/main/vue/src/router.js

@@ -891,6 +891,22 @@ const router = new Router({
                     meta: {
                        title: '大智慧账户',
                     },
+               },
+                {
+                    path: '/assetLockEdit',
+                    name: 'AssetLockEdit',
+                    component: () => import(/* webpackChunkName: "assetLockEdit" */ '@/views/AssetLockEdit.vue'),
+                    meta: {
+                       title: '锁仓编辑',
+                    },
+                },
+                {
+                    path: '/assetLockList',
+                    name: 'AssetLockList',
+                    component: () => import(/* webpackChunkName: "assetLockList" */ '@/views/AssetLockList.vue'),
+                    meta: {
+                       title: '锁仓',
+                    },
                }
                 /**INSERT_LOCATION**/
             ]

+ 131 - 0
src/main/vue/src/views/AssetLockEdit.vue

@@ -0,0 +1,131 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                删除
+            </el-button>
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form :model="formData" :rules="rules" ref="form" label-width="80px" label-position="right"
+                         size="small"
+                         style="max-width: 500px;">
+                        <el-form-item prop="userId" label="用户ID">
+                                    <el-input-number type="number" v-model="formData.userId"></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="phone" label="手机">
+                                    <el-input v-model="formData.phone"></el-input>
+                        </el-form-item>
+                        <el-form-item prop="nickname" label="昵称">
+                                    <el-input v-model="formData.nickname"></el-input>
+                        </el-form-item>
+                        <el-form-item prop="assetId" label="藏品ID">
+                                    <el-input-number type="number" v-model="formData.assetId"></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="name" label="藏品名称">
+                                    <el-input v-model="formData.name"></el-input>
+                        </el-form-item>
+                        <el-form-item prop="number" label="藏品编号">
+                                    <el-input-number type="number" v-model="formData.number"></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="lockAt" label="起始时间">
+                                    <el-date-picker
+                                            v-model="formData.lockAt"
+                                            type="datetime"
+                                            value-format="yyyy-MM-dd HH:mm:ss"
+                                            placeholder="选择日期时间">
+                                    </el-date-picker>
+                        </el-form-item>
+                        <el-form-item prop="lockTo" label="结束时间">
+                                    <el-date-picker
+                                            v-model="formData.lockTo"
+                                            type="datetime"
+                                            value-format="yyyy-MM-dd HH:mm:ss"
+                                            placeholder="选择日期时间">
+                                    </el-date-picker>
+                        </el-form-item>
+                    <el-form-item class="form-submit">
+                        <el-button @click="onSave" :loading="saving" type="primary">
+                            保存
+                        </el-button>
+                        <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                            删除
+                        </el-button>
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+    export default {
+        name: 'AssetLockEdit',
+        created() {
+            if (this.$route.query.id) {
+                this.$http
+                    .get('assetLock/get/' + this.$route.query.id)
+                    .then(res => {
+                        this.formData = res;
+                    })
+                    .catch(e => {
+                        console.log(e);
+                        this.$message.error(e.error);
+                    });
+            }
+        },
+        data() {
+            return {
+                saving: false,
+                formData: {
+                },
+                rules: {
+                },
+            }
+        },
+        methods: {
+            onSave() {
+                this.$refs.form.validate((valid) => {
+                    if (valid) {
+                        this.submit();
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            submit() {
+                let data = {...this.formData};
+
+                this.saving = true;
+                this.$http
+                    .post('/assetLock/save', data, {body: 'json'})
+                    .then(res => {
+                        this.saving = false;
+                        this.$message.success('成功');
+                        this.$router.go(-1);
+                    })
+                    .catch(e => {
+                        console.log(e);
+                        this.saving = false;
+                        this.$message.error(e.error);
+                    });
+            },
+            onDelete() {
+                this.$confirm('删除将无法恢复,确认要删除么?', '警告', {type: 'error'}).then(() => {
+                    return this.$http.post(`/assetLock/del/${this.formData.id}`)
+                }).then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                }).catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                })
+            },
+        }
+    }
+</script>
+<style lang="less" scoped></style>

+ 174 - 0
src/main/vue/src/views/AssetLockList.vue

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

+ 5 - 0
src/main/vue/yarn.lock

@@ -3526,6 +3526,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
     readable-stream "^2.0.0"
     stream-shift "^1.0.0"
 
+duration-fns@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/duration-fns/-/duration-fns-3.0.1.tgz#72fc3400ee8c511ee36a19edf26bf49104d4c051"
+  integrity sha512-GHxED15UbbjNyCwk8OBGDs5KV+Zn/cloq66c4IPUslC3JfbbXncW9gtxfpG9b0xDVpVkPFzqy6lZaq9iyqff3Q==
+
 easy-stack@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz"

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

@@ -38,6 +38,7 @@ import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.RandomUtils;
 import org.apache.commons.lang3.Range;
+import org.apache.commons.lang3.time.DurationFormatUtils;
 import org.apache.commons.text.CaseUtils;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.utils.URLEncodedUtils;
@@ -90,6 +91,7 @@ import java.security.spec.X509EncodedKeySpec;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
+import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -735,4 +737,15 @@ public class CommonTest {
     public void testOneKey() {
         UmengUtils.getMobile("62b8106188ccdf4b7eaba3e8", "eyJjIjoibEVjUGh6WXhwdE5FTHVjQ1p0XC9TREZuMGpLSzlZenZhblBaeUxPUWFrRlJUaFwvaWdrUFA2NU1GZGFDd3F3cjdZVGlPWXhEOVRnWGx3XG5PbGI5MytnN29aaGFpV24rbmd2Q1MxUE1vem5cLzVIQ1BveEk4WG94Zkd3Nkozc3pjMk95NUdFZjBsak9Qd09nMk1iVDBWRjl3Y05zMFxuc2NScGVTd0VoSXh1elpwZ2c5dk5xQlliN2toNWF0cmRtRHQzanZNTHBjUm5OeCtwMXdKS1wvN1VlYTN4b3FJajhSakRuVnFZVmJrcm1cbmtKSDhkNUxrbEEzSzhXU2pVeEF5bkZlSVRWbGNpRlJHTUhvZTBYM1ppZ0pDa0pQWk80VVFGVktSaWlRcFpQbGVGTEhNOHRpTnlTQ0Ncbm1ESmQ1OEM3eWdmWktcLzJcL1ZUc1ZkdFpYUktcL0FqVjNGbk84QnhTZWZQZjVmZEI0YThJQzE2SVFINW91Y2U3NERRbG96SGVIVjhSMldcbmQzaHFRZVh4UFkwd09mdW51elFGSHRscUxnSXlWUDA2bFRaZ0VYdTZpbjJDWDlSUndpa3RxUU81NkRjYzdCeWEzdXQyN0ZcL0VzUmRjXG5Vc3J3ZjVkNE41K0N6cFlBSlpWZmh4ZUlYMUZUVmJPZmFEcWQ0RUpPSTFnclwvTkpXUW9uSG14cUZvXC81WEM2dGwzSG5lQWZwM3dGejJcbk1qQ3d4aWF1ZTJ6OWVpNXZpb1F6Z0lhTWREckJ0eFFPcDU3RW5sZm50NGF1NG8yNGJSQTBSSzI2THZwRzVEakRiZWRTY2xZVEZSY2tcbjBITlUzUFdiQ1hrPVxuIiwiayI6IlQwd0hqc3JiSlV6XC9YRGNUSDVcL0Q5QkpcLzZaVzZmR1BGckdQcmRjdEZDR1Z0NGtcLzdTTzJ6TGU5dlVcL0JlUitcL0VYcDNTWFlLcWhLYXh2QW1vazZjY3FveGk3aE1xckcySHNwbUxLWEtkXC9CbUlHMHhDUjVPWm42SWJvK1BhU2djWlk0MUJic01zRm9RVmRmYUIrZitTVkdJQU50K2lNdm5SRkJpUXNja1N0SVZFd0hvZFgwVkZwd2xVN1UwRTBER01qclJLclI2eGRsMFlSZURIbTVuN3hqaVppOVg1ZTQzZ1UxYk9GOVwvelJwa2ZVd05sN3g2a0tpS25XQ25TU2FuQjhDbHJiNTZ0Qll2czdcL3pKdDJsYXh6UGVMb0Jielp1N0lLTHlUcnc4ZVwvVlNmaUlPYmI5dVNqeXFLRU12dXNQRHFmNmNlQUpRUzlIcW1nQk0wbWZiOVE9PSIsIm8iOiJBbmRyb2lkIn0=");
     }
+
+    @Test
+    public void parseDuration() {
+        System.out.println(Duration.parse("P1DT1H30M100S").toString());
+    }
+
+    @Test
+    public void formatDuration(){
+        System.out.println(DurationFormatUtils.formatDuration(Duration.parse("P1D").toMillis(),
+                "H小时mm分ss秒", true));
+    }
 }