refactor(api): 更新缓存标签处理和API调用逻辑

- 在 revalidate 路由中引入 isAllowedCacheTag 函数,优化无效标签过滤逻辑。
- 更新评论和点赞相关组件,替换 API 调用为新的 action 函数,提升代码一致性。
- 在多个页面中使用缓存 API 以提高数据获取效率,简化 API 请求逻辑。
This commit is contained in:
刘宇阳
2026-07-08 10:58:58 +08:00
parent 1703a19243
commit be71c15b3c
18 changed files with 217 additions and 41 deletions

36
src/actions/article.ts Normal file
View File

@@ -0,0 +1,36 @@
'use server';
import { refresh, updateTag } from 'next/cache';
import { Request } from '@/utils/request';
import { CACHE_TAGS } from '@/lib/cache-tags';
import { Comment } from '@/types/app/comment';
export async function likeArticleAction(id: number, count: number) {
const result = await Request<number>('POST', `/article/${id}/like`, { count });
if (result.code === 200) {
updateTag(`${CACHE_TAGS.article}-${id}`);
updateTag(CACHE_TAGS.articles);
}
return result;
}
export async function addArticleCommentAction(data: Comment) {
const result = await Request('POST', '/comment', data);
// 如果请求成功则更新缓存
if (data.articleId && result.code === 200) {
// 更新文章详情缓存
updateTag(`${CACHE_TAGS.article}-${data.articleId}`);
// 更新文章列表缓存
updateTag(CACHE_TAGS.articles);
// 更新文章评论列表缓存
updateTag(`${CACHE_TAGS.comments}-${data.articleId}`);
// 触发页面重新渲染
refresh();
}
return result;
}

37
src/actions/record.ts Normal file
View File

@@ -0,0 +1,37 @@
'use server';
import { refresh, updateTag } from 'next/cache';
import { Request } from '@/utils/request';
import { CACHE_TAGS } from '@/lib/cache-tags';
import { RecordComment } from '@/types/app/recordComment';
export async function likeRecordAction(id: number, count: number) {
const result = await Request<number>('POST', `/record/${id}/like`, { count });
if (result.code === 200) {
updateTag(CACHE_TAGS.records);
updateTag(`${CACHE_TAGS.record}-${id}`);
refresh();
}
return result;
}
export async function addRecordCommentAction(data: RecordComment) {
const result = await Request('POST', '/record/comment', data);
// 如果请求成功则更新缓存
if (data.recordId && result.code === 200) {
// 更新说说列表缓存
updateTag(CACHE_TAGS.records);
// 更新说说详情缓存
updateTag(`${CACHE_TAGS.record}-${data.recordId}`);
// 更新说说评论列表缓存
updateTag(`${CACHE_TAGS.comments}-${data.recordId}`);
// 触发页面重新渲染
refresh();
}
return result;
}

25
src/actions/wall.ts Normal file
View File

@@ -0,0 +1,25 @@
'use server';
import { refresh, updateTag } from 'next/cache';
import { Request } from '@/utils/request';
import { CACHE_TAGS } from '@/lib/cache-tags';
import { Wall } from '@/types/app/wall';
export async function addWallAction(data: Wall) {
const result = await Request('POST', '/wall', data);
// 如果请求成功则更新缓存
if (result.code === 200) {
// 更新留言列表缓存
updateTag(CACHE_TAGS.walls);
// 更新留言分类缓存
if (data.cateId) {
updateTag(`${CACHE_TAGS.wall}-${data.cateId}`);
}
// 触发页面重新渲染
refresh();
}
return result;
}

21
src/actions/web.ts Normal file
View File

@@ -0,0 +1,21 @@
'use server';
import { refresh, updateTag } from 'next/cache';
import { Request } from '@/utils/request';
import { CACHE_TAGS } from '@/lib/cache-tags';
import { Web } from '@/types/app/web';
export async function addWebAction(data: Web) {
const result = await Request('POST', '/link', data);
// 如果请求成功则更新缓存
if (result.code === 200) {
// 更新友链列表缓存
updateTag(CACHE_TAGS.webs);
// 触发页面重新渲染
refresh();
}
return result;
}

View File

@@ -6,9 +6,7 @@
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
import { CACHE_TAGS } from '@/lib/cache-tags';
const ALLOWED_TAGS = new Set(Object.values(CACHE_TAGS));
import { CACHE_TAGS, isAllowedCacheTag } from '@/lib/cache-tags';
export async function POST(req: NextRequest) {
const secret = req.headers.get('x-revalidate-secret');
@@ -33,8 +31,8 @@ export async function POST(req: NextRequest) {
// 无 body 时使用默认 tags
}
// 过滤掉无效的标签
const invalidTags = tags.filter((tag) => !ALLOWED_TAGS.has(tag as (typeof CACHE_TAGS)[keyof typeof CACHE_TAGS]) && !tag.startsWith(`${CACHE_TAGS.article}-`) && !tag.startsWith(`${CACHE_TAGS.articlesList}-`));
// 过滤掉无效的标签 使用 isAllowedCacheTag 判断是否是允许的缓存标签
const invalidTags = tags.filter((tag) => !isAllowedCacheTag(tag));
// 如果无效的标签,则返回 400 错误
if (invalidTags.length) {
return NextResponse.json({ message: 'Invalid tags', invalidTags }, { status: 400 });

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { addCommentDataAPI } from '@/api/comment';
import { addArticleCommentAction } from '@/actions/article';
import { Bounce, ToastOptions, toast } from 'react-toastify';
import { Spinner, Popover, PopoverTrigger, PopoverContent } from '@/ThriveUI';
import HCaptchaType from '@hcaptcha/react-hcaptcha';
@@ -89,7 +89,7 @@ const CommentForm = ({ articleId }: Props) => {
if (!isNaN(+qq)) data.avatar = `https://q1.qlogo.cn/g?b=qq&nk=${qq}&s=640`;
}
const { code, message } = await addCommentDataAPI({
const { code, message } = await addArticleCommentAction({
...data,
articleId,
commentId: commentId === articleId ? 0 : commentId,

View File

@@ -2,7 +2,7 @@
import { createContext, useContext, type ReactNode } from 'react';
import useDebouncedLike from '@/hooks/useDebouncedLike';
import { likeArticleAPI } from '@/api/article';
import { likeArticleAction } from '@/actions/article';
import LikeButtonCore from '@/components/LikeButton/LikeButtonCore';
interface ArticleLikeContextValue {
@@ -27,7 +27,7 @@ export function ArticleLikeProvider({
initialCount?: number;
children: ReactNode;
}) {
const value = useDebouncedLike(articleId, initialCount, likeArticleAPI);
const value = useDebouncedLike(articleId, initialCount, likeArticleAction);
return <ArticleLikeContext.Provider value={value}>{children}</ArticleLikeContext.Provider>;
}

View File

@@ -13,7 +13,8 @@ import {
} from '@/ThriveUI';
import { type SubmitHandler } from 'react-hook-form';
import { Web, WebType } from '@/types/app/web';
import { addWebDataAPI, getWebTypeListAPI } from '@/api/web';
import { addWebAction } from '@/actions/web';
import { getWebTypeListAPI } from '@/api/web';
import { Bounce, toast, ToastOptions } from 'react-toastify';
import HCaptchaType from '@hcaptcha/react-hcaptcha';
import HCaptcha from '@/components/HCaptcha';
@@ -50,12 +51,6 @@ export default () => {
};
useEffect(() => {
const message = localStorage.getItem('toastMessage');
if (message) {
toast.success(message, toastConfig);
localStorage.removeItem('toastMessage');
}
getWebTypeList();
}, []);
@@ -68,7 +63,7 @@ export default () => {
if (hasHCaptcha && !captchaToken) return setCaptchaError('请完成人机验证');
setLoading(true);
const { code, message } = await addWebDataAPI({
const { code, message } = await addWebAction({
...data,
createTime: Date.now().toString(),
h_captcha_response: captchaToken!,
@@ -83,9 +78,9 @@ export default () => {
setCaptchaError('');
setCaptchaToken(null);
captchaRef.current?.resetCaptcha();
methods.reset({} as Web);
localStorage.setItem('toastMessage', '🎉 提交成功, 请等待审核!');
window.location.reload();
toast.success('🎉 提交成功, 请等待审核!', toastConfig);
onClose();
};

View File

@@ -1,6 +1,7 @@
import { Metadata } from 'next';
import { getWebListAPI, getWebTypeListAPI } from '@/api/web';
import { getWebListCacheAPI } from '@/lib/web';
import { getWebTypeListAPI } from '@/api/web';
import { Web as WebLink } from '@/types/app/web';
import Friend from './index';
@@ -11,7 +12,7 @@ export const metadata: Metadata = {
};
export default async () => {
const linkRes = await getWebListAPI();
const linkRes = await getWebListCacheAPI();
const typeRes = await getWebTypeListAPI();
const linkList = linkRes?.data ?? [];
const typeList = typeRes?.data ?? [];

View File

@@ -10,7 +10,8 @@ import { RiMessage3Line } from 'react-icons/ri';
import RandomAvatar from '@/components/RandomAvatar';
import HCaptcha from '@/components/HCaptcha';
import Show from '@/components/Show';
import { addRecordCommentDataAPI, getRecordCommentListAPI } from '@/api/recordComment';
import { addRecordCommentAction } from '@/actions/record';
import { getRecordCommentListAPI } from '@/api/recordComment';
import { RecordComment } from '@/types/app/recordComment';
import { useAppConfig } from '@/components/AppConfigProvider';
@@ -122,7 +123,7 @@ export default function RecordCommentPanel({ recordId, onCountChange }: Props) {
}
}
const { code, message } = await addRecordCommentDataAPI({
const { code, message } = await addRecordCommentAction({
...data,
recordId,
commentId,

View File

@@ -5,7 +5,7 @@ import { RiChat3Line } from 'react-icons/ri';
import ImageList from './ImageList';
import RecordCommentPanel from './Comment';
import LikeButton from '@/components/LikeButton';
import { likeRecordAPI } from '@/api/record';
import { likeRecordAction } from '@/actions/record';
import { getRecordCommentListAPI } from '@/api/recordComment';
import { getRelativeTimeLabel } from '@/utils';
import { User } from '@/types/app/user';
@@ -73,7 +73,7 @@ export default function RecordCard({ id, content, images, likeCount, mood, locat
</div>
<div className="flex shrink-0 items-center gap-3 rounded-lg bg-[#f4f6f9] px-2 py-0.5 dark:bg-white/5">
<LikeButton entityId={Number(id)} initialCount={likeCount ?? 0} likeAPI={likeRecordAPI} variant="inline" className="gap-1" />
<LikeButton entityId={Number(id)} initialCount={likeCount ?? 0} likeAPI={likeRecordAction} variant="inline" className="gap-1" />
<span className="h-3.5 w-px bg-slate-200 dark:bg-white/10" aria-hidden />
<button
type="button"

View File

@@ -1,5 +1,5 @@
import { Metadata } from 'next';
import { getRecordListAPI } from '@/api/record';
import { getRecordListCacheAPI } from '@/lib/record';
import { getAuthorDataAPI } from '@/api/user';
import RecordPageClient from './components/RecordPageClient';
@@ -11,7 +11,7 @@ export const metadata: Metadata = {
export default async () => {
const [userRes, recordRes] = await Promise.all([
getAuthorDataAPI(),
getRecordListAPI({ pageNum: 1, pageSize: 8 }),
getRecordListCacheAPI({ pageNum: 1, pageSize: 8 }),
]);
return (

View File

@@ -1,7 +1,7 @@
import { Metadata } from 'next';
import { getCateListAPI, getCateWallListAPI } from '@/api/wall';
import { getCateListAPI } from '@/api/wall';
import { getCateWallListCacheAPI } from '@/lib/wall';
import WallPageClient from '../components/WallPageClient';
export const metadata: Metadata = {
title: '💌 留言墙',
description: '💌 留言墙',
@@ -17,7 +17,7 @@ export default async (props: Props) => {
const sorted = [...(cateList ?? [])].sort((a, b) => a.order - b.order);
const activeCate = cate || sorted[0]?.mark || '';
const cateId = sorted.find((item) => item.mark === activeCate)?.id ?? sorted[0]?.id ?? 0;
const { data: wallsData } = await getCateWallListAPI(cateId);
const { data: wallsData } = await getCateWallListCacheAPI(cateId);
return (
<WallPageClient

View File

@@ -16,7 +16,8 @@ import {
} from '@/ThriveUI';
import { type SubmitHandler } from 'react-hook-form';
import { Cate, Wall } from '@/types/app/wall';
import { addWallDataAPI, getCateListAPI } from '@/api/wall';
import { addWallAction } from '@/actions/wall';
import { getCateListAPI } from '@/api/wall';
import { Bounce, toast, ToastContainer, ToastOptions } from 'react-toastify';
import HCaptchaType from '@hcaptcha/react-hcaptcha';
import HCaptcha from '@/components/HCaptcha';
@@ -52,12 +53,6 @@ export default () => {
};
useEffect(() => {
const message = localStorage.getItem('toastMessage');
if (message) {
toast.success(message, toastConfig);
localStorage.removeItem('toastMessage');
}
getCateList();
}, []);
@@ -69,7 +64,7 @@ export default () => {
if (hasHCaptcha && !captchaToken) return setCaptchaError('请完成人机验证');
const { code, message } = await addWallDataAPI({
const { code, message } = await addWallAction({
...data,
createTime: Date.now().toString(),
h_captcha_response: captchaToken!,
@@ -83,9 +78,9 @@ export default () => {
setCaptchaError('');
setCaptchaToken(null);
captchaRef.current?.resetCaptcha();
methods.reset({ color: '#ffe3944d' } as Wall);
localStorage.setItem('toastMessage', '🎉 提交成功, 请等待审核!');
window.location.reload();
toast.success('🎉 提交成功, 请等待审核!', toastConfig);
onClose();
};

View File

@@ -7,4 +7,30 @@ export const CACHE_TAGS = {
articles: 'articles',
article: 'article',
articlesList: 'articles-list',
} as const;
records: 'records',
record: 'record',
walls: 'walls',
wall: 'wall',
webs: 'webs',
comments: 'comments',
} as const;
// 动态缓存标签前缀
const DYNAMIC_PREFIXES = [
`${CACHE_TAGS.article}-`,
`${CACHE_TAGS.articlesList}-`,
`${CACHE_TAGS.record}-`,
`${CACHE_TAGS.wall}-`,
`${CACHE_TAGS.comments}-`,
] as const;
// 判断是否是允许的缓存标签
export function isAllowedCacheTag(tag: string) {
// 如果标签是 CACHE_TAGS 中的值,则返回 true
return (
(Object.values(CACHE_TAGS) as string[]).includes(tag) ||
// 如果标签是 DYNAMIC_PREFIXES 中的值,则返回 true
DYNAMIC_PREFIXES.some((prefix) => tag.startsWith(prefix))
// 如果标签是 CACHE_TAGS 中的值,或者 DYNAMIC_PREFIXES 中的值,则返回 true
);
}

15
src/lib/record.ts Normal file
View File

@@ -0,0 +1,15 @@
import { cacheLife, cacheTag } from 'next/cache';
import { getRecordListAPI } from '@/api/record';
import { CACHE_TAGS } from '@/lib/cache-tags';
export async function getRecordListCacheAPI(params: Page = {}) {
'use cache';
const pageNum = params.pageNum ?? 1;
const pageSize = params.pageSize ?? 8;
cacheLife('blog');
cacheTag(CACHE_TAGS.records);
return getRecordListAPI(params);
}

13
src/lib/wall.ts Normal file
View File

@@ -0,0 +1,13 @@
import { cacheLife, cacheTag } from 'next/cache';
import { getCateWallListAPI } from '@/api/wall';
import { CACHE_TAGS } from '@/lib/cache-tags';
export async function getCateWallListCacheAPI(cateId: number) {
'use cache';
cacheLife('blog');
cacheTag(CACHE_TAGS.walls, `${CACHE_TAGS.wall}-${cateId}`);
return getCateWallListAPI(cateId);
}

13
src/lib/web.ts Normal file
View File

@@ -0,0 +1,13 @@
import { cacheLife, cacheTag } from 'next/cache';
import { getWebListAPI } from '@/api/web';
import { CACHE_TAGS } from '@/lib/cache-tags';
export async function getWebListCacheAPI() {
'use cache';
cacheLife('blog');
cacheTag(CACHE_TAGS.webs);
return getWebListAPI();
}