feat: 自定义临时访问链接的有效时长

This commit is contained in:
xiaozzzi 2024-01-31 22:54:31 +08:00
parent b4e001e3e9
commit 90fa7071f3
8 changed files with 192 additions and 25 deletions

View File

@ -289,13 +289,19 @@ public class ArticleController {
/**
* 创建文章的临时访问缓存
*
* @param id 文章ID
* @param id 文章ID
* @param duration 临时访问的过期时间
* @return 临时访问Key
* @since 1.9.0
*/
@GetMapping("/temp/key")
public R<String> createTempVisitKey(@RequestParam("id") Long id) {
return R.ok(tempVisitService.create(id, AuthContext.getUserId()));
public R<String> createTempVisitKey(@RequestParam("id") Long id,
@RequestParam(value = "duration", required = false) Long duration) {
if (duration == null) {
duration = 3 * 60L;
}
log.info("创建文章临时访问权限 [{}:{}m]", id, duration);
return R.ok(tempVisitService.create(id, AuthContext.getUserId(), duration));
}
/**

View File

@ -3,6 +3,7 @@ package com.blossom.backend.server.article.draft;
import cn.hutool.core.util.ObjUtil;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.util.security.SHA256Util;
import com.blossom.common.cache.caffeine.DynamicExpiry;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
@ -10,6 +11,7 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -24,25 +26,31 @@ public class ArticleTempVisitService {
/**
* 存放文章ID的缓存
*
* @since 1.13.0 支持动态过期时间
*/
private final Cache<String, TempVisit> tempVisitCache = Caffeine.newBuilder()
.expireAfter(new DynamicExpiry())
.initialCapacity(500)
.expireAfterWrite(3, TimeUnit.HOURS)
.removalListener((String key, TempVisit value, RemovalCause cause) ->
log.info("临时访问文章 [" + value.getArticleId() + "] 被删除")
log.info("remove temp visit articleId [" + Objects.requireNonNull(value).getArticleId() + "]")
)
.build();
/**
* 生成一个缓存 key, key 并非文章的摘要码
* 生成一个缓存 key, key 并非文章的摘要码,
*
* @param articleId 文章ID
* @param userId 用户ID
* @param duration 过期时间, 单位分钟
* @return 缓存 key
*/
public String create(Long articleId, Long userId) {
public String create(Long articleId, Long userId, Long duration) {
XzException400.throwBy(ObjUtil.isNull(articleId), "文章ID为必填项");
String key = SHA256Util.encode(UUID.randomUUID().toString());
tempVisitCache.put(key, new TempVisit(articleId, userId));
tempVisitCache.policy().expireVariably().ifPresent(e -> {
e.put(key, new TempVisit(articleId, userId), duration, TimeUnit.MINUTES);
});
return key;
}

View File

@ -0,0 +1,28 @@
package com.blossom.common.cache.caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* key 使用不同的过期时间
*
* @since 1.13.0
*/
public class DynamicExpiry implements Expiry<String, Object> {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull Object value, long currentTime) {
return 0;
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
}

View File

@ -1,4 +1,4 @@
package com.blossom.common.base.caffeine;
package com.blossom.common.cache.caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
@ -13,25 +13,31 @@ import java.util.concurrent.TimeUnit;
public class Test {
private static final Cache<String, String> cache = Caffeine.newBuilder()
.expireAfter(new DynamicExpiry())
.initialCapacity(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
.removalListener((String key, String value, RemovalCause cause) -> System.out.println(key + " 被删除"))
.removalListener((String key, String value, RemovalCause cause) -> log.info(key + " 被删除"))
.build();
private static final ScheduledExecutorService clearUpScheduled = Executors.newScheduledThreadPool(1);
public Test() {
clearUpScheduled.scheduleWithFixedDelay(this::clear, 5, 1, TimeUnit.SECONDS);
clearUpScheduled.scheduleWithFixedDelay(this::clear, 0, 1, TimeUnit.SECONDS);
}
private void clear() {
log.info("尝试过期缓存");
log.info("删除");
cache.cleanUp();
}
public static void main(String[] args) {
Test test = new Test();
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
cache.policy().expireVariably().ifPresent(e -> {
e.put("A1", "1", 7, TimeUnit.SECONDS);
});
cache.policy().expireVariably().ifPresent(e -> {
e.put("A2", "2", 3, TimeUnit.SECONDS);
});
Thread.sleep(20 * 1000);
}
}

View File

@ -120,4 +120,5 @@
.el-popper.is-small {
padding: 0 8px;
box-shadow: 1px 1px 3px #f4f4f4;
}

View File

@ -0,0 +1,92 @@
<template>
<div class="article-custom-temp-visit-root">
<!-- 标题 -->
<div class="info-title">
<div class="iconbl bl-visit"></div>
<div class="article-name">{{ articleInfo.articleName }}</div>
</div>
<div class="content">
<div class="duration">有效时长(分钟) <el-input-number v-model="duration" min="1" controls-position="right"></el-input-number></div>
<div class="expire">将在 {{ expire }} 后失效</div>
<div class="btns">
<el-button size="default" type="primary" @click="create">创建访问链接</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useServerStore } from '@renderer/stores/server'
import { articleTempKey, articleTempH } from '@renderer/api/blossom'
import { writeText } from '@renderer/assets/utils/electron'
import dayjs from 'dayjs'
const server = useServerStore()
const duration = ref(180)
const expire = computed(() => {
return dayjs().add(duration.value, 'minutes').format('YYYY-MM-DD HH:mm')
})
const articleInfo = ref({
articleId: '',
articleName: ''
})
const create = () => {
articleTempKey({ id: articleInfo.value.articleId, duration: duration.value }).then((resp) => {
let url = server.serverUrl + articleTempH + resp.data
writeText(url)
ElMessage.info({ type: 'info', message: '已复制链接' })
emits('created')
})
}
const reload = (articleName: string, articleId: string) => {
articleInfo.value = {
articleId: articleId,
articleName: articleName
}
}
defineExpose({ reload })
const emits = defineEmits(['created'])
</script>
<style scoped lang="scss">
@import '@renderer/assets/styles/bl-dialog-info';
.article-custom-temp-visit-root {
border-radius: 10px;
@include box(100%, 100%);
.article-name {
width: 320px;
@include font(16px, 500);
@include ellipsis();
}
.content {
padding: 10px;
text-align: center;
.duration {
}
.expire {
@include font(14px, 300);
width: 100%;
padding: 10px;
border-radius: 8px;
background-color: var(--bl-preview-code-bg-color);
margin: 13px 0;
}
.btns {
}
}
}
</style>

View File

@ -99,7 +99,6 @@
<div v-else class="doc-trees-placeholder">暂无文档可点击上方 添加</div>
</div>
<!-- 右键菜单, 添加到 body -->
<Teleport to="body">
<div
v-show="rMenu.show"
@ -111,13 +110,11 @@
<div @click="rename"><span class="iconbl bl-pen"></span>重命名</div>
<div @click="handleShowDocInfoDialog('upd')"><span class="iconbl bl-a-fileedit-line"></span>编辑详情</div>
<div v-if="curDoc.ty === 3" @click="syncDoc()"><span class="iconbl bl-a-cloudrefresh-line"></span>同步文章</div>
<!-- <div v-if="curDoc.ty != 3" @click="handleShowDocInfoDialog('add', curDoc.i)"><span class="iconbl bl-a-fileadd-fill"></span>新增子级文档</div> -->
<div v-if="curDoc.ty !== 3" @click="addFolder"><span class="iconbl bl-a-fileadd-line"></span>新增文件夹</div>
<div v-if="curDoc.ty !== 3" @click="addArticle"><span class="iconbl bl-a-fileadd-fill"></span>新增笔记</div>
<div v-if="curDoc.ty === 3" @click="createUrl('link')"><span class="iconbl bl-correlation-line"></span>复制双链引用</div>
<div v-if="curDoc.ty !== 3" @click="handleShowArticleImportDialog()"><span class="iconbl bl-file-upload-line"></span>导入文章</div>
<!-- 更多二级菜单 -->
<div @mouseenter="handleHoverRightMenuLevel2($event, 2)" data-bl-prevet="true">
<span class="iconbl bl-a-rightsmallline-line"></span>
<span class="iconbl bl-apps-line"></span>更多
@ -146,7 +143,6 @@
<div v-if="curDoc.ty === 3" @click="openArticleWindow"><span class="iconbl bl-a-computerend-line"></span>新窗口查看</div>
<div v-if="curDoc.ty === 3" @click="createUrl('tempVisit', true)"><span class="iconbl bl-visit"></span>浏览器临时访问</div>
<!-- 导出及二级菜单 -->
<div v-if="curDoc.ty === 3" @mouseenter="handleHoverRightMenuLevel2($event, 4)" data-bl-prevet="true">
<span class="iconbl bl-a-rightsmallline-line"></span>
<span class="iconbl bl-file-download-line"></span>导出文章
@ -159,10 +155,11 @@
</div>
<div v-if="curDoc.ty === 3" @mouseenter="handleHoverRightMenuLevel2($event, 2)" data-bl-prevet="true">
<span class="iconbl bl-a-rightsmallline-line"></span>
<span class="iconbl bl-a-linkspread-line"></span>复制链接
<span class="iconbl bl-a-linkspread-line"></span>创建链接
<div class="menu-content-level2" :style="rMenuLevel2">
<div v-if="curDoc.o === 1" @click="createUrl('copy')"><span class="iconbl bl-planet-line"></span>复制博客链接</div>
<div @click="createUrl('tempVisit')"><span class="iconbl bl-visit"></span>复制临时访问链接</div>
<div v-if="curDoc.o === 1" @click="createUrl('copy')"><span class="iconbl bl-planet-line"></span>复制博客地址</div>
<div @click="createUrl('tempVisit')"><span class="iconbl bl-visit"></span>创建临时访问(3h)</div>
<div @click="handleShowACustomTempVisitDialog"><span class="iconbl bl-visit"></span>创建临时访问(自定义)</div>
</div>
</div>
<div v-if="curDoc.ty === 3 && curDoc.o === 1" @click="createUrl('open')"><span class="iconbl bl-planet-line"></span>博客中查看</div>
@ -224,6 +221,18 @@
:close-on-click-modal="true">
<ArticleSearch @open-article="openArticle" @create-link="createUrlLink"></ArticleSearch>
</el-dialog>
<!-- 自定义临时访问链接 -->
<el-dialog
v-model="isShowACustomTempVisitDialog"
width="400"
style="height: 200px"
:align-center="true"
:append-to-body="true"
:destroy-on-close="true"
:close-on-click-modal="true">
<ArticleCustomTempVisit ref="ArticleCustomTempVisitRef" @created="tempVisitCreated"></ArticleCustomTempVisit>
</el-dialog>
</template>
<script setup lang="ts">
@ -232,7 +241,7 @@ import { useServerStore } from '@renderer/stores/server'
import { useUserStore } from '@renderer/stores/user'
import { useConfigStore } from '@renderer/stores/config'
import { ref, provide, onBeforeUnmount, nextTick, computed } from 'vue'
import { ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { MenuInstance } from 'element-plus'
import { ArrowDownBold, ArrowRightBold } from '@element-plus/icons-vue'
import {
@ -270,6 +279,7 @@ import ArticleQrCode from './ArticleQrCode.vue'
import ArticleInfo from './ArticleInfo.vue'
import ArticleImport from './ArticleImport.vue'
import ArticleSearch from './ArticleSearch.vue'
import ArticleCustomTempVisit from './ArticleCustomTempVisit.vue'
const server = useServerStore()
const user = useUserStore()
@ -804,6 +814,22 @@ const openArticle = (article: DocTree) => {
})
}
//#endregion
//#region ----------------------------------------< 访 >--------------------------------------
const isShowACustomTempVisitDialog = ref(false)
const ArticleCustomTempVisitRef = ref()
const handleShowACustomTempVisitDialog = () => {
isShowACustomTempVisitDialog.value = true
nextTick(() => {
ArticleCustomTempVisitRef.value.reload(curDoc.value.n, curDoc.value.i)
})
}
const tempVisitCreated = () => {
isShowACustomTempVisitDialog.value = false
}
//#endregion
const clickCurDoc = (tree: DocTree) => {
emits('clickDoc', tree)
}

View File

@ -29,8 +29,8 @@ window.blconfig = {
* 服务器的地址
*/
DOMAIN: {
// 将该值填写为你的后台访问地址, 与 blossom 客户端登录页面填写的地址相同
PRD: 'http://localhost:9999',
// 将该值填写为你的后台访问地址, 与 blossom 客户端登录 页面填写的地址相同
PRD: 'http://192.168.31.6:9999',
// 将该值填写你开放为博客的用户ID
USER_ID: 1
},